mirror of
https://github.com/freqtrade/freqtrade.git
synced 2024-09-20 09:31:12 +00:00
Merge branch 'freqtrade:develop' into develop
This commit is contained in:
commit
d748cf6531
|
@ -7,7 +7,7 @@ from typing import Dict, List, Optional, Tuple
|
|||
from pandas import DataFrame, concat
|
||||
|
||||
from freqtrade.configuration import TimeRange
|
||||
from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS
|
||||
from freqtrade.constants import DATETIME_PRINT_FORMAT, DEFAULT_DATAFRAME_COLUMNS
|
||||
from freqtrade.data.converter import (clean_ohlcv_dataframe, ohlcv_to_dataframe,
|
||||
trades_remove_duplicates, trades_to_ohlcv)
|
||||
from freqtrade.data.history.idatahandler import IDataHandler, get_datahandler
|
||||
|
@ -227,9 +227,11 @@ def _download_pair_history(pair: str, *,
|
|||
)
|
||||
|
||||
logger.debug("Current Start: %s",
|
||||
f"{data.iloc[0]['date']:DATETIME_PRINT_FORMAT}" if not data.empty else 'None')
|
||||
f"{data.iloc[0]['date']:{DATETIME_PRINT_FORMAT}}"
|
||||
if not data.empty else 'None')
|
||||
logger.debug("Current End: %s",
|
||||
f"{data.iloc[-1]['date']:DATETIME_PRINT_FORMAT}" if not data.empty else 'None')
|
||||
f"{data.iloc[-1]['date']:{DATETIME_PRINT_FORMAT}}"
|
||||
if not data.empty else 'None')
|
||||
|
||||
# Default since_ms to 30 days if nothing is given
|
||||
new_data = exchange.get_historic_ohlcv(pair=pair,
|
||||
|
@ -252,10 +254,12 @@ def _download_pair_history(pair: str, *,
|
|||
data = clean_ohlcv_dataframe(concat([data, new_dataframe], axis=0), timeframe, pair,
|
||||
fill_missing=False, drop_incomplete=False)
|
||||
|
||||
logger.debug("New Start: %s",
|
||||
f"{data.iloc[0]['date']:DATETIME_PRINT_FORMAT}" if not data.empty else 'None')
|
||||
logger.debug("New Start: %s",
|
||||
f"{data.iloc[0]['date']:{DATETIME_PRINT_FORMAT}}"
|
||||
if not data.empty else 'None')
|
||||
logger.debug("New End: %s",
|
||||
f"{data.iloc[-1]['date']:DATETIME_PRINT_FORMAT}" if not data.empty else 'None')
|
||||
f"{data.iloc[-1]['date']:{DATETIME_PRINT_FORMAT}}"
|
||||
if not data.empty else 'None')
|
||||
|
||||
data_handler.ohlcv_store(pair, timeframe, data=data, candle_type=candle_type)
|
||||
return True
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from enum import Enum
|
||||
|
||||
|
||||
class MarginMode(Enum):
|
||||
class MarginMode(str, Enum):
|
||||
"""
|
||||
Enum to distinguish between
|
||||
cross margin/futures margin_mode and
|
||||
|
|
|
@ -1148,8 +1148,8 @@ class Exchange:
|
|||
else:
|
||||
limit_rate = stop_price * (2 - limit_price_pct)
|
||||
|
||||
bad_stop_price = ((stop_price <= limit_rate) if side ==
|
||||
"sell" else (stop_price >= limit_rate))
|
||||
bad_stop_price = ((stop_price < limit_rate) if side ==
|
||||
"sell" else (stop_price > limit_rate))
|
||||
# Ensure rate is less than stop price
|
||||
if bad_stop_price:
|
||||
# This can for example happen if the stop / liquidation price is set to 0
|
||||
|
|
|
@ -20,6 +20,7 @@ from pandas import DataFrame
|
|||
from freqtrade.configuration import TimeRange
|
||||
from freqtrade.constants import Config
|
||||
from freqtrade.data.history import load_pair_history
|
||||
from freqtrade.enums import CandleType
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
|
||||
from freqtrade.strategy.interface import IStrategy
|
||||
|
@ -639,7 +640,7 @@ class FreqaiDataDrawer:
|
|||
pair=pair,
|
||||
timerange=timerange,
|
||||
data_format=self.config.get("dataformat_ohlcv", "json"),
|
||||
candle_type=self.config.get("trading_mode", "spot"),
|
||||
candle_type=self.config.get("candle_type_def", CandleType.SPOT),
|
||||
)
|
||||
|
||||
def get_base_and_corr_dataframes(
|
||||
|
|
|
@ -12,7 +12,7 @@ from freqtrade.constants import Config, ListPairsWithTimeframes
|
|||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange.types import Tickers
|
||||
from freqtrade.misc import plural
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter
|
||||
from freqtrade.util import PeriodicCache, dt_floor_day, dt_now, dt_ts
|
||||
|
||||
|
||||
|
@ -68,6 +68,27 @@ class AgeFilter(IPairList):
|
|||
f"{self._max_days_listed} {plural(self._max_days_listed, 'day')}"
|
||||
) if self._max_days_listed else '')
|
||||
|
||||
@staticmethod
|
||||
def description() -> str:
|
||||
return "Filter pairs by age (days listed)."
|
||||
|
||||
@staticmethod
|
||||
def available_parameters() -> Dict[str, PairlistParameter]:
|
||||
return {
|
||||
"min_days_listed": {
|
||||
"type": "number",
|
||||
"default": 10,
|
||||
"description": "Minimum Days Listed",
|
||||
"help": "Minimum number of days a pair must have been listed on the exchange.",
|
||||
},
|
||||
"max_days_listed": {
|
||||
"type": "number",
|
||||
"default": None,
|
||||
"description": "Maximum Days Listed",
|
||||
"help": "Maximum number of days a pair must have been listed on the exchange.",
|
||||
},
|
||||
}
|
||||
|
||||
def filter_pairlist(self, pairlist: List[str], tickers: Tickers) -> List[str]:
|
||||
"""
|
||||
:param pairlist: pairlist to filter or sort
|
||||
|
|
|
@ -4,7 +4,7 @@ PairList Handler base class
|
|||
import logging
|
||||
from abc import ABC, abstractmethod, abstractproperty
|
||||
from copy import deepcopy
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Any, Dict, List, Literal, Optional, TypedDict, Union
|
||||
|
||||
from freqtrade.constants import Config
|
||||
from freqtrade.exceptions import OperationalException
|
||||
|
@ -16,8 +16,44 @@ from freqtrade.mixins import LoggingMixin
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class __PairlistParameterBase(TypedDict):
|
||||
description: str
|
||||
help: str
|
||||
|
||||
|
||||
class __NumberPairlistParameter(__PairlistParameterBase):
|
||||
type: Literal["number"]
|
||||
default: Union[int, float, None]
|
||||
|
||||
|
||||
class __StringPairlistParameter(__PairlistParameterBase):
|
||||
type: Literal["string"]
|
||||
default: Union[str, None]
|
||||
|
||||
|
||||
class __OptionPairlistParameter(__PairlistParameterBase):
|
||||
type: Literal["option"]
|
||||
default: Union[str, None]
|
||||
options: List[str]
|
||||
|
||||
|
||||
class __BoolPairlistParameter(__PairlistParameterBase):
|
||||
type: Literal["boolean"]
|
||||
default: Union[bool, None]
|
||||
|
||||
|
||||
PairlistParameter = Union[
|
||||
__NumberPairlistParameter,
|
||||
__StringPairlistParameter,
|
||||
__OptionPairlistParameter,
|
||||
__BoolPairlistParameter
|
||||
]
|
||||
|
||||
|
||||
class IPairList(LoggingMixin, ABC):
|
||||
|
||||
is_pairlist_generator = False
|
||||
|
||||
def __init__(self, exchange: Exchange, pairlistmanager,
|
||||
config: Config, pairlistconfig: Dict[str, Any],
|
||||
pairlist_pos: int) -> None:
|
||||
|
@ -53,6 +89,37 @@ class IPairList(LoggingMixin, ABC):
|
|||
If no Pairlist requires tickers, an empty Dict is passed
|
||||
as tickers argument to filter_pairlist
|
||||
"""
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def description() -> str:
|
||||
"""
|
||||
Return description of this Pairlist Handler
|
||||
-> Please overwrite in subclasses
|
||||
"""
|
||||
return ""
|
||||
|
||||
@staticmethod
|
||||
def available_parameters() -> Dict[str, PairlistParameter]:
|
||||
"""
|
||||
Return parameters used by this Pairlist Handler, and their type
|
||||
contains a dictionary with the parameter name as key, and a dictionary
|
||||
with the type and default value.
|
||||
-> Please overwrite in subclasses
|
||||
"""
|
||||
return {}
|
||||
|
||||
@staticmethod
|
||||
def refresh_period_parameter() -> Dict[str, PairlistParameter]:
|
||||
return {
|
||||
"refresh_period": {
|
||||
"type": "number",
|
||||
"default": 1800,
|
||||
"description": "Refresh period",
|
||||
"help": "Refresh period in seconds",
|
||||
}
|
||||
}
|
||||
|
||||
@abstractmethod
|
||||
def short_desc(self) -> str:
|
||||
|
|
|
@ -7,7 +7,7 @@ from typing import Any, Dict, List
|
|||
from freqtrade.constants import Config
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange.types import Tickers
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -43,6 +43,27 @@ class OffsetFilter(IPairList):
|
|||
return f"{self.name} - Taking {self._number_pairs} Pairs, starting from {self._offset}."
|
||||
return f"{self.name} - Offsetting pairs by {self._offset}."
|
||||
|
||||
@staticmethod
|
||||
def description() -> str:
|
||||
return "Offset pair list filter."
|
||||
|
||||
@staticmethod
|
||||
def available_parameters() -> Dict[str, PairlistParameter]:
|
||||
return {
|
||||
"offset": {
|
||||
"type": "number",
|
||||
"default": 0,
|
||||
"description": "Offset",
|
||||
"help": "Offset of the pairlist.",
|
||||
},
|
||||
"number_assets": {
|
||||
"type": "number",
|
||||
"default": 0,
|
||||
"description": "Number of assets",
|
||||
"help": "Number of assets to use from the pairlist, starting from offset.",
|
||||
},
|
||||
}
|
||||
|
||||
def filter_pairlist(self, pairlist: List[str], tickers: Tickers) -> List[str]:
|
||||
"""
|
||||
Filters and sorts pairlist and returns the whitelist again.
|
||||
|
|
|
@ -9,7 +9,7 @@ import pandas as pd
|
|||
from freqtrade.constants import Config
|
||||
from freqtrade.exchange.types import Tickers
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -40,6 +40,27 @@ class PerformanceFilter(IPairList):
|
|||
"""
|
||||
return f"{self.name} - Sorting pairs by performance."
|
||||
|
||||
@staticmethod
|
||||
def description() -> str:
|
||||
return "Filter pairs by performance."
|
||||
|
||||
@staticmethod
|
||||
def available_parameters() -> Dict[str, PairlistParameter]:
|
||||
return {
|
||||
"minutes": {
|
||||
"type": "number",
|
||||
"default": 0,
|
||||
"description": "Minutes",
|
||||
"help": "Consider trades from the last X minutes. 0 means all trades.",
|
||||
},
|
||||
"min_profit": {
|
||||
"type": "number",
|
||||
"default": None,
|
||||
"description": "Minimum profit",
|
||||
"help": "Minimum profit in percent. Pairs with less profit are removed.",
|
||||
},
|
||||
}
|
||||
|
||||
def filter_pairlist(self, pairlist: List[str], tickers: Tickers) -> List[str]:
|
||||
"""
|
||||
Filters and sorts pairlist and returns the allowlist again.
|
||||
|
|
|
@ -46,6 +46,10 @@ class PrecisionFilter(IPairList):
|
|||
"""
|
||||
return f"{self.name} - Filtering untradable pairs."
|
||||
|
||||
@staticmethod
|
||||
def description() -> str:
|
||||
return "Filters low-value coins which would not allow setting stoplosses."
|
||||
|
||||
def _validate_pair(self, pair: str, ticker: Optional[Ticker]) -> bool:
|
||||
"""
|
||||
Check if pair has enough room to add a stoploss to avoid "unsellable" buys of very
|
||||
|
|
|
@ -7,7 +7,7 @@ from typing import Any, Dict, Optional
|
|||
from freqtrade.constants import Config
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange.types import Ticker
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -65,6 +65,40 @@ class PriceFilter(IPairList):
|
|||
|
||||
return f"{self.name} - No price filters configured."
|
||||
|
||||
@staticmethod
|
||||
def description() -> str:
|
||||
return "Filter pairs by price."
|
||||
|
||||
@staticmethod
|
||||
def available_parameters() -> Dict[str, PairlistParameter]:
|
||||
return {
|
||||
"low_price_ratio": {
|
||||
"type": "number",
|
||||
"default": 0,
|
||||
"description": "Low price ratio",
|
||||
"help": ("Remove pairs where a price move of 1 price unit (pip) "
|
||||
"is above this ratio."),
|
||||
},
|
||||
"min_price": {
|
||||
"type": "number",
|
||||
"default": 0,
|
||||
"description": "Minimum price",
|
||||
"help": "Remove pairs with a price below this value.",
|
||||
},
|
||||
"max_price": {
|
||||
"type": "number",
|
||||
"default": 0,
|
||||
"description": "Maximum price",
|
||||
"help": "Remove pairs with a price above this value.",
|
||||
},
|
||||
"max_value": {
|
||||
"type": "number",
|
||||
"default": 0,
|
||||
"description": "Maximum value",
|
||||
"help": "Remove pairs with a value (price * amount) above this value.",
|
||||
},
|
||||
}
|
||||
|
||||
def _validate_pair(self, pair: str, ticker: Optional[Ticker]) -> bool:
|
||||
"""
|
||||
Check if if one price-step (pip) is > than a certain barrier.
|
||||
|
|
|
@ -8,7 +8,7 @@ from typing import Any, Dict, List, Optional
|
|||
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange.types import Tickers
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -28,6 +28,7 @@ class ProducerPairList(IPairList):
|
|||
}
|
||||
],
|
||||
"""
|
||||
is_pairlist_generator = True
|
||||
|
||||
def __init__(self, exchange, pairlistmanager,
|
||||
config: Dict[str, Any], pairlistconfig: Dict[str, Any],
|
||||
|
@ -56,6 +57,28 @@ class ProducerPairList(IPairList):
|
|||
"""
|
||||
return f"{self.name} - {self._producer_name}"
|
||||
|
||||
@staticmethod
|
||||
def description() -> str:
|
||||
return "Get a pairlist from an upstream bot."
|
||||
|
||||
@staticmethod
|
||||
def available_parameters() -> Dict[str, PairlistParameter]:
|
||||
return {
|
||||
"number_assets": {
|
||||
"type": "number",
|
||||
"default": 0,
|
||||
"description": "Number of assets",
|
||||
"help": "Number of assets to use from the pairlist",
|
||||
},
|
||||
"producer_name": {
|
||||
"type": "string",
|
||||
"default": "default",
|
||||
"description": "Producer name",
|
||||
"help": ("Name of the producer to use. Requires additional "
|
||||
"external_message_consumer configuration.")
|
||||
},
|
||||
}
|
||||
|
||||
def _filter_pairlist(self, pairlist: Optional[List[str]]):
|
||||
upstream_pairlist = self._pairlistmanager._dataprovider.get_producer_pairs(
|
||||
self._producer_name)
|
||||
|
|
|
@ -15,7 +15,7 @@ from freqtrade import __version__
|
|||
from freqtrade.constants import Config
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange.types import Tickers
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -23,6 +23,8 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
class RemotePairList(IPairList):
|
||||
|
||||
is_pairlist_generator = True
|
||||
|
||||
def __init__(self, exchange, pairlistmanager,
|
||||
config: Config, pairlistconfig: Dict[str, Any],
|
||||
pairlist_pos: int) -> None:
|
||||
|
@ -63,6 +65,46 @@ class RemotePairList(IPairList):
|
|||
"""
|
||||
return f"{self.name} - {self._pairlistconfig['number_assets']} pairs from RemotePairlist."
|
||||
|
||||
@staticmethod
|
||||
def description() -> str:
|
||||
return "Retrieve pairs from a remote API."
|
||||
|
||||
@staticmethod
|
||||
def available_parameters() -> Dict[str, PairlistParameter]:
|
||||
return {
|
||||
"number_assets": {
|
||||
"type": "number",
|
||||
"default": 0,
|
||||
"description": "Number of assets",
|
||||
"help": "Number of assets to use from the pairlist.",
|
||||
},
|
||||
"pairlist_url": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "URL to fetch pairlist from",
|
||||
"help": "URL to fetch pairlist from",
|
||||
},
|
||||
**IPairList.refresh_period_parameter(),
|
||||
"keep_pairlist_on_failure": {
|
||||
"type": "boolean",
|
||||
"default": True,
|
||||
"description": "Keep last pairlist on failure",
|
||||
"help": "Keep last pairlist on failure",
|
||||
},
|
||||
"read_timeout": {
|
||||
"type": "number",
|
||||
"default": 60,
|
||||
"description": "Read timeout",
|
||||
"help": "Request timeout for remote pairlist",
|
||||
},
|
||||
"bearer_token": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "Bearer token",
|
||||
"help": "Bearer token - used for auth against the upstream service.",
|
||||
},
|
||||
}
|
||||
|
||||
def process_json(self, jsonparse) -> List[str]:
|
||||
|
||||
pairlist = jsonparse.get('pairs', [])
|
||||
|
|
|
@ -9,7 +9,7 @@ from freqtrade.constants import Config
|
|||
from freqtrade.enums import RunMode
|
||||
from freqtrade.exchange import timeframe_to_seconds
|
||||
from freqtrade.exchange.types import Tickers
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter
|
||||
from freqtrade.util.periodic_cache import PeriodicCache
|
||||
|
||||
|
||||
|
@ -55,6 +55,28 @@ class ShuffleFilter(IPairList):
|
|||
return (f"{self.name} - Shuffling pairs every {self._shuffle_freq}" +
|
||||
(f", seed = {self._seed}." if self._seed is not None else "."))
|
||||
|
||||
@staticmethod
|
||||
def description() -> str:
|
||||
return "Randomize pairlist order."
|
||||
|
||||
@staticmethod
|
||||
def available_parameters() -> Dict[str, PairlistParameter]:
|
||||
return {
|
||||
"shuffle_frequency": {
|
||||
"type": "option",
|
||||
"default": "candle",
|
||||
"options": ["candle", "iteration"],
|
||||
"description": "Shuffle frequency",
|
||||
"help": "Shuffle frequency. Can be either 'candle' or 'iteration'.",
|
||||
},
|
||||
"seed": {
|
||||
"type": "number",
|
||||
"default": None,
|
||||
"description": "Random Seed",
|
||||
"help": "Seed for random number generator. Not used in live mode.",
|
||||
},
|
||||
}
|
||||
|
||||
def filter_pairlist(self, pairlist: List[str], tickers: Tickers) -> List[str]:
|
||||
"""
|
||||
Filters and sorts pairlist and returns the whitelist again.
|
||||
|
|
|
@ -7,7 +7,7 @@ from typing import Any, Dict, Optional
|
|||
from freqtrade.constants import Config
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange.types import Ticker
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -45,6 +45,21 @@ class SpreadFilter(IPairList):
|
|||
return (f"{self.name} - Filtering pairs with ask/bid diff above "
|
||||
f"{self._max_spread_ratio:.2%}.")
|
||||
|
||||
@staticmethod
|
||||
def description() -> str:
|
||||
return "Filter by bid/ask difference."
|
||||
|
||||
@staticmethod
|
||||
def available_parameters() -> Dict[str, PairlistParameter]:
|
||||
return {
|
||||
"max_spread_ratio": {
|
||||
"type": "number",
|
||||
"default": 0.005,
|
||||
"description": "Max spread ratio",
|
||||
"help": "Max spread ratio for a pair to be considered.",
|
||||
},
|
||||
}
|
||||
|
||||
def _validate_pair(self, pair: str, ticker: Optional[Ticker]) -> bool:
|
||||
"""
|
||||
Validate spread for the ticker
|
||||
|
|
|
@ -9,7 +9,7 @@ from typing import Any, Dict, List
|
|||
|
||||
from freqtrade.constants import Config
|
||||
from freqtrade.exchange.types import Tickers
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -17,6 +17,8 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
class StaticPairList(IPairList):
|
||||
|
||||
is_pairlist_generator = True
|
||||
|
||||
def __init__(self, exchange, pairlistmanager,
|
||||
config: Config, pairlistconfig: Dict[str, Any],
|
||||
pairlist_pos: int) -> None:
|
||||
|
@ -40,6 +42,21 @@ class StaticPairList(IPairList):
|
|||
"""
|
||||
return f"{self.name}"
|
||||
|
||||
@staticmethod
|
||||
def description() -> str:
|
||||
return "Use pairlist as configured in config."
|
||||
|
||||
@staticmethod
|
||||
def available_parameters() -> Dict[str, PairlistParameter]:
|
||||
return {
|
||||
"allow_inactive": {
|
||||
"type": "boolean",
|
||||
"default": False,
|
||||
"description": "Allow inactive pairs",
|
||||
"help": "Allow inactive pairs to be in the whitelist.",
|
||||
},
|
||||
}
|
||||
|
||||
def gen_pairlist(self, tickers: Tickers) -> List[str]:
|
||||
"""
|
||||
Generate the pairlist
|
||||
|
|
|
@ -15,7 +15,7 @@ from freqtrade.constants import Config, ListPairsWithTimeframes
|
|||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange.types import Tickers
|
||||
from freqtrade.misc import plural
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter
|
||||
from freqtrade.util import dt_floor_day, dt_now, dt_ts
|
||||
|
||||
|
||||
|
@ -64,6 +64,34 @@ class VolatilityFilter(IPairList):
|
|||
f"{self._min_volatility}-{self._max_volatility} "
|
||||
f" the last {self._days} {plural(self._days, 'day')}.")
|
||||
|
||||
@staticmethod
|
||||
def description() -> str:
|
||||
return "Filter pairs by their recent volatility."
|
||||
|
||||
@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,
|
||||
"description": "Minimum Volatility",
|
||||
"help": "Minimum volatility a pair must have to be considered.",
|
||||
},
|
||||
"max_volatility": {
|
||||
"type": "number",
|
||||
"default": None,
|
||||
"description": "Maximum Volatility",
|
||||
"help": "Maximum volatility a pair must have to be considered.",
|
||||
},
|
||||
**IPairList.refresh_period_parameter()
|
||||
}
|
||||
|
||||
def filter_pairlist(self, pairlist: List[str], tickers: Tickers) -> List[str]:
|
||||
"""
|
||||
Validate trading range
|
||||
|
|
|
@ -14,7 +14,7 @@ from freqtrade.exceptions import OperationalException
|
|||
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_prev_date
|
||||
from freqtrade.exchange.types import Tickers
|
||||
from freqtrade.misc import format_ms_time
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter
|
||||
from freqtrade.util import dt_now
|
||||
|
||||
|
||||
|
@ -26,6 +26,8 @@ SORT_VALUES = ['quoteVolume']
|
|||
|
||||
class VolumePairList(IPairList):
|
||||
|
||||
is_pairlist_generator = True
|
||||
|
||||
def __init__(self, exchange, pairlistmanager,
|
||||
config: Config, pairlistconfig: Dict[str, Any],
|
||||
pairlist_pos: int) -> None:
|
||||
|
@ -112,6 +114,53 @@ class VolumePairList(IPairList):
|
|||
"""
|
||||
return f"{self.name} - top {self._pairlistconfig['number_assets']} volume pairs."
|
||||
|
||||
@staticmethod
|
||||
def description() -> str:
|
||||
return "Provides dynamic pair list based on trade volumes."
|
||||
|
||||
@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",
|
||||
},
|
||||
"sort_key": {
|
||||
"type": "option",
|
||||
"default": "quoteVolume",
|
||||
"options": SORT_VALUES,
|
||||
"description": "Sort key",
|
||||
"help": "Sort key to use for sorting the pairlist.",
|
||||
},
|
||||
"min_value": {
|
||||
"type": "number",
|
||||
"default": 0,
|
||||
"description": "Minimum value",
|
||||
"help": "Minimum 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.",
|
||||
},
|
||||
}
|
||||
|
||||
def gen_pairlist(self, tickers: Tickers) -> List[str]:
|
||||
"""
|
||||
Generate the pairlist
|
||||
|
|
|
@ -13,7 +13,7 @@ from freqtrade.constants import Config, ListPairsWithTimeframes
|
|||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange.types import Tickers
|
||||
from freqtrade.misc import plural
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter
|
||||
from freqtrade.util import dt_floor_day, dt_now, dt_ts
|
||||
|
||||
|
||||
|
@ -62,6 +62,34 @@ class RangeStabilityFilter(IPairList):
|
|||
f"{self._min_rate_of_change}{max_rate_desc} over the "
|
||||
f"last {plural(self._days, 'day')}.")
|
||||
|
||||
@staticmethod
|
||||
def description() -> str:
|
||||
return "Filters pairs by their rate of change."
|
||||
|
||||
@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,
|
||||
"description": "Minimum Rate of Change",
|
||||
"help": "Minimum rate of change to filter pairs.",
|
||||
},
|
||||
"max_rate_of_change": {
|
||||
"type": "number",
|
||||
"default": None,
|
||||
"description": "Maximum Rate of Change",
|
||||
"help": "Maximum rate of change to filter pairs.",
|
||||
},
|
||||
**IPairList.refresh_period_parameter()
|
||||
}
|
||||
|
||||
def filter_pairlist(self, pairlist: List[str], tickers: Tickers) -> List[str]:
|
||||
"""
|
||||
Validate trading range
|
||||
|
|
145
freqtrade/rpc/api_server/api_background_tasks.py
Normal file
145
freqtrade/rpc/api_server/api_background_tasks.py
Normal file
|
@ -0,0 +1,145 @@
|
|||
import logging
|
||||
from copy import deepcopy
|
||||
|
||||
from fastapi import APIRouter, BackgroundTasks, Depends
|
||||
from fastapi.exceptions import HTTPException
|
||||
|
||||
from freqtrade.constants import Config
|
||||
from freqtrade.enums import CandleType
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.rpc.api_server.api_schemas import (BackgroundTaskStatus, BgJobStarted,
|
||||
ExchangeModePayloadMixin, PairListsPayload,
|
||||
PairListsResponse, WhitelistEvaluateResponse)
|
||||
from freqtrade.rpc.api_server.deps import get_config, get_exchange
|
||||
from freqtrade.rpc.api_server.webserver_bgwork import ApiBG
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Private API, protected by authentication and webserver_mode dependency
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get('/background/{jobid}', response_model=BackgroundTaskStatus, tags=['webserver'])
|
||||
def background_job(jobid: str):
|
||||
if not (job := ApiBG.jobs.get(jobid)):
|
||||
raise HTTPException(status_code=404, detail='Job not found.')
|
||||
|
||||
return {
|
||||
'job_id': jobid,
|
||||
'job_category': job['category'],
|
||||
'status': job['status'],
|
||||
'running': job['is_running'],
|
||||
'progress': job.get('progress'),
|
||||
# 'job_error': job['error'],
|
||||
}
|
||||
|
||||
|
||||
@router.get('/pairlists/available',
|
||||
response_model=PairListsResponse, tags=['pairlists', 'webserver'])
|
||||
def list_pairlists(config=Depends(get_config)):
|
||||
from freqtrade.resolvers import PairListResolver
|
||||
pairlists = PairListResolver.search_all_objects(
|
||||
config, False)
|
||||
pairlists = sorted(pairlists, key=lambda x: x['name'])
|
||||
|
||||
return {'pairlists': [{
|
||||
"name": x['name'],
|
||||
"is_pairlist_generator": x['class'].is_pairlist_generator,
|
||||
"params": x['class'].available_parameters(),
|
||||
"description": x['class'].description(),
|
||||
} for x in pairlists
|
||||
]}
|
||||
|
||||
|
||||
def __run_pairlist(job_id: str, config_loc: Config):
|
||||
try:
|
||||
|
||||
ApiBG.jobs[job_id]['is_running'] = True
|
||||
from freqtrade.plugins.pairlistmanager import PairListManager
|
||||
|
||||
exchange = get_exchange(config_loc)
|
||||
pairlists = PairListManager(exchange, config_loc)
|
||||
pairlists.refresh_pairlist()
|
||||
ApiBG.jobs[job_id]['result'] = {
|
||||
'method': pairlists.name_list,
|
||||
'length': len(pairlists.whitelist),
|
||||
'whitelist': pairlists.whitelist
|
||||
}
|
||||
ApiBG.jobs[job_id]['status'] = 'success'
|
||||
except (OperationalException, Exception) as e:
|
||||
logger.exception(e)
|
||||
ApiBG.jobs[job_id]['error'] = str(e)
|
||||
finally:
|
||||
ApiBG.jobs[job_id]['is_running'] = False
|
||||
ApiBG.jobs[job_id]['status'] = 'failed'
|
||||
ApiBG.pairlist_running = False
|
||||
|
||||
|
||||
@router.post('/pairlists/evaluate', response_model=BgJobStarted, tags=['pairlists', 'webserver'])
|
||||
def pairlists_evaluate(payload: PairListsPayload, background_tasks: BackgroundTasks,
|
||||
config=Depends(get_config)):
|
||||
if ApiBG.pairlist_running:
|
||||
raise HTTPException(status_code=400, detail='Pairlist evaluation is already running.')
|
||||
|
||||
config_loc = deepcopy(config)
|
||||
config_loc['stake_currency'] = payload.stake_currency
|
||||
config_loc['pairlists'] = payload.pairlists
|
||||
handleExchangePayload(payload, config_loc)
|
||||
# TODO: overwrite blacklist? make it optional and fall back to the one in config?
|
||||
# Outcome depends on the UI approach.
|
||||
config_loc['exchange']['pair_blacklist'] = payload.blacklist
|
||||
# Random job id
|
||||
job_id = ApiBG.get_job_id()
|
||||
|
||||
ApiBG.jobs[job_id] = {
|
||||
'category': 'pairlist',
|
||||
'status': 'pending',
|
||||
'progress': None,
|
||||
'is_running': False,
|
||||
'result': {},
|
||||
'error': None,
|
||||
}
|
||||
background_tasks.add_task(__run_pairlist, job_id, config_loc)
|
||||
ApiBG.pairlist_running = True
|
||||
|
||||
return {
|
||||
'status': 'Pairlist evaluation started in background.',
|
||||
'job_id': job_id,
|
||||
}
|
||||
|
||||
|
||||
def handleExchangePayload(payload: ExchangeModePayloadMixin, config_loc: Config):
|
||||
"""
|
||||
Handle exchange and trading mode payload.
|
||||
Updates the configuration with the payload values.
|
||||
"""
|
||||
if payload.exchange:
|
||||
config_loc['exchange']['name'] = payload.exchange
|
||||
if payload.trading_mode:
|
||||
config_loc['trading_mode'] = payload.trading_mode
|
||||
config_loc['candle_type_def'] = CandleType.get_default(
|
||||
config_loc.get('trading_mode', 'spot') or 'spot')
|
||||
if payload.margin_mode:
|
||||
config_loc['margin_mode'] = payload.margin_mode
|
||||
|
||||
|
||||
@router.get('/pairlists/evaluate/{jobid}', response_model=WhitelistEvaluateResponse,
|
||||
tags=['pairlists', 'webserver'])
|
||||
def pairlists_evaluate_get(jobid: str):
|
||||
if not (job := ApiBG.jobs.get(jobid)):
|
||||
raise HTTPException(status_code=404, detail='Job not found.')
|
||||
|
||||
if job['is_running']:
|
||||
raise HTTPException(status_code=400, detail='Job not finished yet.')
|
||||
|
||||
if error := job['error']:
|
||||
return {
|
||||
'status': 'failed',
|
||||
'error': error,
|
||||
}
|
||||
|
||||
return {
|
||||
'status': 'success',
|
||||
'result': job['result'],
|
||||
}
|
|
@ -4,10 +4,16 @@ from typing import Any, Dict, List, Optional, Union
|
|||
from pydantic import BaseModel
|
||||
|
||||
from freqtrade.constants import DATETIME_PRINT_FORMAT, IntOrInf
|
||||
from freqtrade.enums import OrderTypeValues, SignalDirection, TradingMode
|
||||
from freqtrade.enums import MarginMode, OrderTypeValues, SignalDirection, TradingMode
|
||||
from freqtrade.types import ValidExchangesType
|
||||
|
||||
|
||||
class ExchangeModePayloadMixin(BaseModel):
|
||||
trading_mode: Optional[TradingMode]
|
||||
margin_mode: Optional[MarginMode]
|
||||
exchange: Optional[str]
|
||||
|
||||
|
||||
class Ping(BaseModel):
|
||||
status: str
|
||||
|
||||
|
@ -28,6 +34,23 @@ class StatusMsg(BaseModel):
|
|||
status: str
|
||||
|
||||
|
||||
class BgJobStarted(StatusMsg):
|
||||
job_id: str
|
||||
|
||||
|
||||
class BackgroundTaskStatus(BaseModel):
|
||||
job_id: str
|
||||
job_category: str
|
||||
status: str
|
||||
running: bool
|
||||
progress: Optional[float]
|
||||
|
||||
|
||||
class BackgroundTaskResult(BaseModel):
|
||||
error: Optional[str]
|
||||
status: str
|
||||
|
||||
|
||||
class ResultMsg(BaseModel):
|
||||
result: str
|
||||
|
||||
|
@ -377,6 +400,10 @@ class WhitelistResponse(BaseModel):
|
|||
method: List[str]
|
||||
|
||||
|
||||
class WhitelistEvaluateResponse(BackgroundTaskResult):
|
||||
result: Optional[WhitelistResponse]
|
||||
|
||||
|
||||
class DeleteTrade(BaseModel):
|
||||
cancel_order_count: int
|
||||
result: str
|
||||
|
@ -401,6 +428,23 @@ class ExchangeListResponse(BaseModel):
|
|||
exchanges: List[ValidExchangesType]
|
||||
|
||||
|
||||
class PairListResponse(BaseModel):
|
||||
name: str
|
||||
description: str
|
||||
is_pairlist_generator: bool
|
||||
params: Dict[str, Any]
|
||||
|
||||
|
||||
class PairListsResponse(BaseModel):
|
||||
pairlists: List[PairListResponse]
|
||||
|
||||
|
||||
class PairListsPayload(ExchangeModePayloadMixin, BaseModel):
|
||||
pairlists: List[Dict[str, Any]]
|
||||
blacklist: List[str]
|
||||
stake_currency: str
|
||||
|
||||
|
||||
class FreqAIModelListResponse(BaseModel):
|
||||
freqaimodels: List[str]
|
||||
|
||||
|
|
|
@ -48,7 +48,8 @@ logger = logging.getLogger(__name__)
|
|||
# 2.27: Add /trades/<id>/reload endpoint
|
||||
# 2.28: Switch reload endpoint to Post
|
||||
# 2.29: Add /exchanges endpoint
|
||||
API_VERSION = 2.29
|
||||
# 2.30: new /pairlists endpoint
|
||||
API_VERSION = 2.30
|
||||
|
||||
# Public API, requires no auth.
|
||||
router_public = APIRouter()
|
||||
|
|
|
@ -3,6 +3,7 @@ from uuid import uuid4
|
|||
|
||||
from fastapi import Depends, HTTPException
|
||||
|
||||
from freqtrade.constants import Config
|
||||
from freqtrade.enums import RunMode
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.persistence.models import _request_id_ctx_var
|
||||
|
@ -43,12 +44,21 @@ def get_api_config() -> Dict[str, Any]:
|
|||
return ApiServer._config['api_server']
|
||||
|
||||
|
||||
def _generate_exchange_key(config: Config) -> str:
|
||||
"""
|
||||
Exchange key - used for caching the exchange object.
|
||||
"""
|
||||
return f"{config['exchange']['name']}_{config.get('trading_mode', 'spot')}"
|
||||
|
||||
|
||||
def get_exchange(config=Depends(get_config)):
|
||||
if not ApiBG.exchange:
|
||||
exchange_key = _generate_exchange_key(config)
|
||||
if not (exchange := ApiBG.exchanges.get(exchange_key)):
|
||||
from freqtrade.resolvers import ExchangeResolver
|
||||
ApiBG.exchange = ExchangeResolver.load_exchange(
|
||||
exchange = ExchangeResolver.load_exchange(
|
||||
config, load_leverage_tiers=False)
|
||||
return ApiBG.exchange
|
||||
ApiBG.exchanges[exchange_key] = exchange
|
||||
return exchange
|
||||
|
||||
|
||||
def get_message_stream():
|
||||
|
|
|
@ -114,6 +114,7 @@ class ApiServer(RPCHandler):
|
|||
|
||||
def configure_app(self, app: FastAPI, config):
|
||||
from freqtrade.rpc.api_server.api_auth import http_basic_or_jwt_token, router_login
|
||||
from freqtrade.rpc.api_server.api_background_tasks import router as api_bg_tasks
|
||||
from freqtrade.rpc.api_server.api_backtest import router as api_backtest
|
||||
from freqtrade.rpc.api_server.api_v1 import router as api_v1
|
||||
from freqtrade.rpc.api_server.api_v1 import router_public as api_v1_public
|
||||
|
@ -130,6 +131,10 @@ class ApiServer(RPCHandler):
|
|||
dependencies=[Depends(http_basic_or_jwt_token),
|
||||
Depends(is_webserver_mode)],
|
||||
)
|
||||
app.include_router(api_bg_tasks, prefix="/api/v1",
|
||||
dependencies=[Depends(http_basic_or_jwt_token),
|
||||
Depends(is_webserver_mode)],
|
||||
)
|
||||
app.include_router(ws_router, prefix="/api/v1")
|
||||
app.include_router(router_login, prefix="/api/v1", tags=["auth"])
|
||||
# UI Router MUST be last!
|
||||
|
|
|
@ -1,5 +1,17 @@
|
|||
|
||||
from typing import Any, Dict
|
||||
from typing import Any, Dict, Literal, Optional, TypedDict
|
||||
from uuid import uuid4
|
||||
|
||||
from freqtrade.exchange.exchange import Exchange
|
||||
|
||||
|
||||
class JobsContainer(TypedDict):
|
||||
category: Literal['pairlist']
|
||||
is_running: bool
|
||||
status: str
|
||||
progress: Optional[float]
|
||||
result: Any
|
||||
error: Optional[str]
|
||||
|
||||
|
||||
class ApiBG():
|
||||
|
@ -13,4 +25,15 @@ class ApiBG():
|
|||
}
|
||||
bgtask_running: bool = False
|
||||
# Exchange - only available in webserver mode.
|
||||
exchange = None
|
||||
exchanges: Dict[str, Exchange] = {}
|
||||
|
||||
# Generic background jobs
|
||||
|
||||
# TODO: Change this to TTLCache
|
||||
jobs: Dict[str, JobsContainer] = {}
|
||||
# Pairlist evaluate things
|
||||
pairlist_running: bool = False
|
||||
|
||||
@staticmethod
|
||||
def get_job_id() -> str:
|
||||
return str(uuid4())
|
||||
|
|
|
@ -314,6 +314,13 @@ class FtRestClient():
|
|||
"""
|
||||
return self._get(f"strategy/{strategy}")
|
||||
|
||||
def pairlists_available(self):
|
||||
"""Lists available pairlist providers
|
||||
|
||||
:return: json object
|
||||
"""
|
||||
return self._get("pairlists/available")
|
||||
|
||||
def plot_config(self):
|
||||
"""Return plot configuration if the strategy defines one.
|
||||
|
||||
|
|
|
@ -3489,6 +3489,27 @@ def test_stoploss_order_unsupported_exchange(default_conf, mocker):
|
|||
exchange.stoploss_adjust(1, {}, side="sell")
|
||||
|
||||
|
||||
@pytest.mark.parametrize('side,ratio,expected', [
|
||||
('sell', 0.99, 99.0), # Default
|
||||
('sell', 0.999, 99.9),
|
||||
('sell', 1, 100),
|
||||
('sell', 1.1, InvalidOrderException),
|
||||
('buy', 0.99, 101.0), # Default
|
||||
('buy', 0.999, 100.1),
|
||||
('buy', 1, 100),
|
||||
('buy', 1.1, InvalidOrderException),
|
||||
])
|
||||
def test__get_stop_limit_rate(default_conf_usdt, mocker, side, ratio, expected):
|
||||
exchange = get_patched_exchange(mocker, default_conf_usdt, id='binance')
|
||||
|
||||
order_types = {'stoploss_on_exchange_limit_ratio': ratio}
|
||||
if isinstance(expected, type) and issubclass(expected, Exception):
|
||||
with pytest.raises(expected):
|
||||
exchange._get_stop_limit_rate(100, order_types, side)
|
||||
else:
|
||||
assert exchange._get_stop_limit_rate(100, order_types, side) == expected
|
||||
|
||||
|
||||
def test_merge_ft_has_dict(default_conf, mocker):
|
||||
mocker.patch.multiple(EXMS,
|
||||
_init_ccxt=MagicMock(return_value=MagicMock()),
|
||||
|
|
|
@ -1657,6 +1657,122 @@ def test_api_freqaimodels(botclient, tmpdir, mocker):
|
|||
]}
|
||||
|
||||
|
||||
def test_api_pairlists_available(botclient, tmpdir):
|
||||
ftbot, client = botclient
|
||||
ftbot.config['user_data_dir'] = Path(tmpdir)
|
||||
|
||||
rc = client_get(client, f"{BASE_URI}/pairlists/available")
|
||||
|
||||
assert_response(rc, 503)
|
||||
assert rc.json()['detail'] == 'Bot is not in the correct state.'
|
||||
|
||||
ftbot.config['runmode'] = RunMode.WEBSERVER
|
||||
|
||||
rc = client_get(client, f"{BASE_URI}/pairlists/available")
|
||||
assert_response(rc)
|
||||
response = rc.json()
|
||||
assert isinstance(response['pairlists'], list)
|
||||
assert len(response['pairlists']) > 0
|
||||
|
||||
assert len([r for r in response['pairlists'] if r['name'] == 'AgeFilter']) == 1
|
||||
assert len([r for r in response['pairlists'] if r['name'] == 'VolumePairList']) == 1
|
||||
assert len([r for r in response['pairlists'] if r['name'] == 'StaticPairList']) == 1
|
||||
|
||||
volumepl = [r for r in response['pairlists'] if r['name'] == 'VolumePairList'][0]
|
||||
assert volumepl['is_pairlist_generator'] is True
|
||||
assert len(volumepl['params']) > 1
|
||||
age_pl = [r for r in response['pairlists'] if r['name'] == 'AgeFilter'][0]
|
||||
assert age_pl['is_pairlist_generator'] is False
|
||||
assert len(volumepl['params']) > 2
|
||||
|
||||
|
||||
def test_api_pairlists_evaluate(botclient, tmpdir, mocker):
|
||||
ftbot, client = botclient
|
||||
ftbot.config['user_data_dir'] = Path(tmpdir)
|
||||
|
||||
rc = client_get(client, f"{BASE_URI}/pairlists/evaluate/randomJob")
|
||||
|
||||
assert_response(rc, 503)
|
||||
assert rc.json()['detail'] == 'Bot is not in the correct state.'
|
||||
|
||||
ftbot.config['runmode'] = RunMode.WEBSERVER
|
||||
|
||||
rc = client_get(client, f"{BASE_URI}/pairlists/evaluate/randomJob")
|
||||
assert_response(rc, 404)
|
||||
assert rc.json()['detail'] == 'Job not found.'
|
||||
|
||||
body = {
|
||||
"pairlists": [
|
||||
{"method": "StaticPairList", },
|
||||
],
|
||||
"blacklist": [
|
||||
],
|
||||
"stake_currency": "BTC"
|
||||
}
|
||||
# Fail, already running
|
||||
ApiBG.pairlist_running = True
|
||||
rc = client_post(client, f"{BASE_URI}/pairlists/evaluate", body)
|
||||
assert_response(rc, 400)
|
||||
assert rc.json()['detail'] == 'Pairlist evaluation is already running.'
|
||||
|
||||
# should start the run
|
||||
ApiBG.pairlist_running = False
|
||||
rc = client_post(client, f"{BASE_URI}/pairlists/evaluate", body)
|
||||
assert_response(rc)
|
||||
assert rc.json()['status'] == 'Pairlist evaluation started in background.'
|
||||
job_id = rc.json()['job_id']
|
||||
|
||||
rc = client_get(client, f"{BASE_URI}/background/RandomJob")
|
||||
assert_response(rc, 404)
|
||||
assert rc.json()['detail'] == 'Job not found.'
|
||||
|
||||
rc = client_get(client, f"{BASE_URI}/background/{job_id}")
|
||||
assert_response(rc)
|
||||
response = rc.json()
|
||||
assert response['job_id'] == job_id
|
||||
assert response['job_category'] == 'pairlist'
|
||||
|
||||
rc = client_get(client, f"{BASE_URI}/pairlists/evaluate/{job_id}")
|
||||
assert_response(rc)
|
||||
response = rc.json()
|
||||
assert response['result']['whitelist'] == ['ETH/BTC', 'LTC/BTC', 'XRP/BTC', 'NEO/BTC',]
|
||||
assert response['result']['length'] == 4
|
||||
|
||||
# Restart with additional filter, reducing the list to 2
|
||||
body['pairlists'].append({"method": "OffsetFilter", "number_assets": 2})
|
||||
rc = client_post(client, f"{BASE_URI}/pairlists/evaluate", body)
|
||||
assert_response(rc)
|
||||
assert rc.json()['status'] == 'Pairlist evaluation started in background.'
|
||||
job_id = rc.json()['job_id']
|
||||
|
||||
rc = client_get(client, f"{BASE_URI}/pairlists/evaluate/{job_id}")
|
||||
assert_response(rc)
|
||||
response = rc.json()
|
||||
assert response['result']['whitelist'] == ['ETH/BTC', 'LTC/BTC', ]
|
||||
assert response['result']['length'] == 2
|
||||
# Patch __run_pairlists
|
||||
plm = mocker.patch('freqtrade.rpc.api_server.api_background_tasks.__run_pairlist',
|
||||
return_value=None)
|
||||
body = {
|
||||
"pairlists": [
|
||||
{"method": "StaticPairList", },
|
||||
],
|
||||
"blacklist": [
|
||||
],
|
||||
"stake_currency": "BTC",
|
||||
"exchange": "randomExchange",
|
||||
"trading_mode": "futures",
|
||||
"margin_mode": "isolated",
|
||||
}
|
||||
rc = client_post(client, f"{BASE_URI}/pairlists/evaluate", body)
|
||||
assert_response(rc)
|
||||
assert plm.call_count == 1
|
||||
call_config = plm.call_args_list[0][0][1]
|
||||
assert call_config['exchange']['name'] == 'randomExchange'
|
||||
assert call_config['trading_mode'] == 'futures'
|
||||
assert call_config['margin_mode'] == 'isolated'
|
||||
|
||||
|
||||
def test_list_available_pairs(botclient):
|
||||
ftbot, client = botclient
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user