mirror of
https://github.com/freqtrade/freqtrade.git
synced 2024-11-10 10:21:59 +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 pandas import DataFrame, concat
|
||||||
|
|
||||||
from freqtrade.configuration import TimeRange
|
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,
|
from freqtrade.data.converter import (clean_ohlcv_dataframe, ohlcv_to_dataframe,
|
||||||
trades_remove_duplicates, trades_to_ohlcv)
|
trades_remove_duplicates, trades_to_ohlcv)
|
||||||
from freqtrade.data.history.idatahandler import IDataHandler, get_datahandler
|
from freqtrade.data.history.idatahandler import IDataHandler, get_datahandler
|
||||||
|
@ -227,9 +227,11 @@ def _download_pair_history(pair: str, *,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.debug("Current Start: %s",
|
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",
|
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
|
# Default since_ms to 30 days if nothing is given
|
||||||
new_data = exchange.get_historic_ohlcv(pair=pair,
|
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,
|
data = clean_ohlcv_dataframe(concat([data, new_dataframe], axis=0), timeframe, pair,
|
||||||
fill_missing=False, drop_incomplete=False)
|
fill_missing=False, drop_incomplete=False)
|
||||||
|
|
||||||
logger.debug("New Start: %s",
|
logger.debug("New 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("New End: %s",
|
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)
|
data_handler.ohlcv_store(pair, timeframe, data=data, candle_type=candle_type)
|
||||||
return True
|
return True
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
class MarginMode(Enum):
|
class MarginMode(str, Enum):
|
||||||
"""
|
"""
|
||||||
Enum to distinguish between
|
Enum to distinguish between
|
||||||
cross margin/futures margin_mode and
|
cross margin/futures margin_mode and
|
||||||
|
|
|
@ -1148,8 +1148,8 @@ class Exchange:
|
||||||
else:
|
else:
|
||||||
limit_rate = stop_price * (2 - limit_price_pct)
|
limit_rate = stop_price * (2 - limit_price_pct)
|
||||||
|
|
||||||
bad_stop_price = ((stop_price <= limit_rate) if side ==
|
bad_stop_price = ((stop_price < limit_rate) if side ==
|
||||||
"sell" else (stop_price >= limit_rate))
|
"sell" else (stop_price > limit_rate))
|
||||||
# Ensure rate is less than stop price
|
# Ensure rate is less than stop price
|
||||||
if bad_stop_price:
|
if bad_stop_price:
|
||||||
# This can for example happen if the stop / liquidation price is set to 0
|
# 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.configuration import TimeRange
|
||||||
from freqtrade.constants import Config
|
from freqtrade.constants import Config
|
||||||
from freqtrade.data.history import load_pair_history
|
from freqtrade.data.history import load_pair_history
|
||||||
|
from freqtrade.enums import CandleType
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
|
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
|
||||||
from freqtrade.strategy.interface import IStrategy
|
from freqtrade.strategy.interface import IStrategy
|
||||||
|
@ -639,7 +640,7 @@ class FreqaiDataDrawer:
|
||||||
pair=pair,
|
pair=pair,
|
||||||
timerange=timerange,
|
timerange=timerange,
|
||||||
data_format=self.config.get("dataformat_ohlcv", "json"),
|
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(
|
def get_base_and_corr_dataframes(
|
||||||
|
|
|
@ -12,7 +12,7 @@ from freqtrade.constants import Config, ListPairsWithTimeframes
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.exchange.types import Tickers
|
from freqtrade.exchange.types import Tickers
|
||||||
from freqtrade.misc import plural
|
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
|
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')}"
|
f"{self._max_days_listed} {plural(self._max_days_listed, 'day')}"
|
||||||
) if self._max_days_listed else '')
|
) 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]:
|
def filter_pairlist(self, pairlist: List[str], tickers: Tickers) -> List[str]:
|
||||||
"""
|
"""
|
||||||
:param pairlist: pairlist to filter or sort
|
:param pairlist: pairlist to filter or sort
|
||||||
|
|
|
@ -4,7 +4,7 @@ PairList Handler base class
|
||||||
import logging
|
import logging
|
||||||
from abc import ABC, abstractmethod, abstractproperty
|
from abc import ABC, abstractmethod, abstractproperty
|
||||||
from copy import deepcopy
|
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.constants import Config
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
|
@ -16,8 +16,44 @@ from freqtrade.mixins import LoggingMixin
|
||||||
logger = logging.getLogger(__name__)
|
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):
|
class IPairList(LoggingMixin, ABC):
|
||||||
|
|
||||||
|
is_pairlist_generator = False
|
||||||
|
|
||||||
def __init__(self, exchange: Exchange, pairlistmanager,
|
def __init__(self, exchange: Exchange, pairlistmanager,
|
||||||
config: Config, pairlistconfig: Dict[str, Any],
|
config: Config, pairlistconfig: Dict[str, Any],
|
||||||
pairlist_pos: int) -> None:
|
pairlist_pos: int) -> None:
|
||||||
|
@ -53,6 +89,37 @@ class IPairList(LoggingMixin, ABC):
|
||||||
If no Pairlist requires tickers, an empty Dict is passed
|
If no Pairlist requires tickers, an empty Dict is passed
|
||||||
as tickers argument to filter_pairlist
|
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
|
@abstractmethod
|
||||||
def short_desc(self) -> str:
|
def short_desc(self) -> str:
|
||||||
|
|
|
@ -7,7 +7,7 @@ from typing import Any, Dict, List
|
||||||
from freqtrade.constants import Config
|
from freqtrade.constants import Config
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.exchange.types import Tickers
|
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__)
|
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} - Taking {self._number_pairs} Pairs, starting from {self._offset}."
|
||||||
return f"{self.name} - Offsetting pairs by {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]:
|
def filter_pairlist(self, pairlist: List[str], tickers: Tickers) -> List[str]:
|
||||||
"""
|
"""
|
||||||
Filters and sorts pairlist and returns the whitelist again.
|
Filters and sorts pairlist and returns the whitelist again.
|
||||||
|
|
|
@ -9,7 +9,7 @@ import pandas as pd
|
||||||
from freqtrade.constants import Config
|
from freqtrade.constants import Config
|
||||||
from freqtrade.exchange.types import Tickers
|
from freqtrade.exchange.types import Tickers
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
from freqtrade.plugins.pairlist.IPairList import IPairList
|
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -40,6 +40,27 @@ class PerformanceFilter(IPairList):
|
||||||
"""
|
"""
|
||||||
return f"{self.name} - Sorting pairs by performance."
|
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]:
|
def filter_pairlist(self, pairlist: List[str], tickers: Tickers) -> List[str]:
|
||||||
"""
|
"""
|
||||||
Filters and sorts pairlist and returns the allowlist again.
|
Filters and sorts pairlist and returns the allowlist again.
|
||||||
|
|
|
@ -46,6 +46,10 @@ class PrecisionFilter(IPairList):
|
||||||
"""
|
"""
|
||||||
return f"{self.name} - Filtering untradable pairs."
|
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:
|
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
|
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.constants import Config
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.exchange.types import Ticker
|
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__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -65,6 +65,40 @@ class PriceFilter(IPairList):
|
||||||
|
|
||||||
return f"{self.name} - No price filters configured."
|
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:
|
def _validate_pair(self, pair: str, ticker: Optional[Ticker]) -> bool:
|
||||||
"""
|
"""
|
||||||
Check if if one price-step (pip) is > than a certain barrier.
|
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.exceptions import OperationalException
|
||||||
from freqtrade.exchange.types import Tickers
|
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__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -28,6 +28,7 @@ class ProducerPairList(IPairList):
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"""
|
"""
|
||||||
|
is_pairlist_generator = True
|
||||||
|
|
||||||
def __init__(self, exchange, pairlistmanager,
|
def __init__(self, exchange, pairlistmanager,
|
||||||
config: Dict[str, Any], pairlistconfig: Dict[str, Any],
|
config: Dict[str, Any], pairlistconfig: Dict[str, Any],
|
||||||
|
@ -56,6 +57,28 @@ class ProducerPairList(IPairList):
|
||||||
"""
|
"""
|
||||||
return f"{self.name} - {self._producer_name}"
|
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]]):
|
def _filter_pairlist(self, pairlist: Optional[List[str]]):
|
||||||
upstream_pairlist = self._pairlistmanager._dataprovider.get_producer_pairs(
|
upstream_pairlist = self._pairlistmanager._dataprovider.get_producer_pairs(
|
||||||
self._producer_name)
|
self._producer_name)
|
||||||
|
|
|
@ -15,7 +15,7 @@ from freqtrade import __version__
|
||||||
from freqtrade.constants import Config
|
from freqtrade.constants import Config
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.exchange.types import Tickers
|
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__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -23,6 +23,8 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
class RemotePairList(IPairList):
|
class RemotePairList(IPairList):
|
||||||
|
|
||||||
|
is_pairlist_generator = True
|
||||||
|
|
||||||
def __init__(self, exchange, pairlistmanager,
|
def __init__(self, exchange, pairlistmanager,
|
||||||
config: Config, pairlistconfig: Dict[str, Any],
|
config: Config, pairlistconfig: Dict[str, Any],
|
||||||
pairlist_pos: int) -> None:
|
pairlist_pos: int) -> None:
|
||||||
|
@ -63,6 +65,46 @@ class RemotePairList(IPairList):
|
||||||
"""
|
"""
|
||||||
return f"{self.name} - {self._pairlistconfig['number_assets']} pairs from RemotePairlist."
|
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]:
|
def process_json(self, jsonparse) -> List[str]:
|
||||||
|
|
||||||
pairlist = jsonparse.get('pairs', [])
|
pairlist = jsonparse.get('pairs', [])
|
||||||
|
|
|
@ -9,7 +9,7 @@ from freqtrade.constants import Config
|
||||||
from freqtrade.enums import RunMode
|
from freqtrade.enums import RunMode
|
||||||
from freqtrade.exchange import timeframe_to_seconds
|
from freqtrade.exchange import timeframe_to_seconds
|
||||||
from freqtrade.exchange.types import Tickers
|
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
|
from freqtrade.util.periodic_cache import PeriodicCache
|
||||||
|
|
||||||
|
|
||||||
|
@ -55,6 +55,28 @@ class ShuffleFilter(IPairList):
|
||||||
return (f"{self.name} - Shuffling pairs every {self._shuffle_freq}" +
|
return (f"{self.name} - Shuffling pairs every {self._shuffle_freq}" +
|
||||||
(f", seed = {self._seed}." if self._seed is not None else "."))
|
(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]:
|
def filter_pairlist(self, pairlist: List[str], tickers: Tickers) -> List[str]:
|
||||||
"""
|
"""
|
||||||
Filters and sorts pairlist and returns the whitelist again.
|
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.constants import Config
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.exchange.types import Ticker
|
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__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -45,6 +45,21 @@ class SpreadFilter(IPairList):
|
||||||
return (f"{self.name} - Filtering pairs with ask/bid diff above "
|
return (f"{self.name} - Filtering pairs with ask/bid diff above "
|
||||||
f"{self._max_spread_ratio:.2%}.")
|
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:
|
def _validate_pair(self, pair: str, ticker: Optional[Ticker]) -> bool:
|
||||||
"""
|
"""
|
||||||
Validate spread for the ticker
|
Validate spread for the ticker
|
||||||
|
|
|
@ -9,7 +9,7 @@ from typing import Any, Dict, List
|
||||||
|
|
||||||
from freqtrade.constants import Config
|
from freqtrade.constants import Config
|
||||||
from freqtrade.exchange.types import Tickers
|
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__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -17,6 +17,8 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
class StaticPairList(IPairList):
|
class StaticPairList(IPairList):
|
||||||
|
|
||||||
|
is_pairlist_generator = True
|
||||||
|
|
||||||
def __init__(self, exchange, pairlistmanager,
|
def __init__(self, exchange, pairlistmanager,
|
||||||
config: Config, pairlistconfig: Dict[str, Any],
|
config: Config, pairlistconfig: Dict[str, Any],
|
||||||
pairlist_pos: int) -> None:
|
pairlist_pos: int) -> None:
|
||||||
|
@ -40,6 +42,21 @@ class StaticPairList(IPairList):
|
||||||
"""
|
"""
|
||||||
return f"{self.name}"
|
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]:
|
def gen_pairlist(self, tickers: Tickers) -> List[str]:
|
||||||
"""
|
"""
|
||||||
Generate the pairlist
|
Generate the pairlist
|
||||||
|
|
|
@ -15,7 +15,7 @@ from freqtrade.constants import Config, ListPairsWithTimeframes
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.exchange.types import Tickers
|
from freqtrade.exchange.types import Tickers
|
||||||
from freqtrade.misc import plural
|
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
|
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"{self._min_volatility}-{self._max_volatility} "
|
||||||
f" the last {self._days} {plural(self._days, 'day')}.")
|
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]:
|
def filter_pairlist(self, pairlist: List[str], tickers: Tickers) -> List[str]:
|
||||||
"""
|
"""
|
||||||
Validate trading range
|
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 import timeframe_to_minutes, timeframe_to_prev_date
|
||||||
from freqtrade.exchange.types import Tickers
|
from freqtrade.exchange.types import Tickers
|
||||||
from freqtrade.misc import format_ms_time
|
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
|
from freqtrade.util import dt_now
|
||||||
|
|
||||||
|
|
||||||
|
@ -26,6 +26,8 @@ SORT_VALUES = ['quoteVolume']
|
||||||
|
|
||||||
class VolumePairList(IPairList):
|
class VolumePairList(IPairList):
|
||||||
|
|
||||||
|
is_pairlist_generator = True
|
||||||
|
|
||||||
def __init__(self, exchange, pairlistmanager,
|
def __init__(self, exchange, pairlistmanager,
|
||||||
config: Config, pairlistconfig: Dict[str, Any],
|
config: Config, pairlistconfig: Dict[str, Any],
|
||||||
pairlist_pos: int) -> None:
|
pairlist_pos: int) -> None:
|
||||||
|
@ -112,6 +114,53 @@ class VolumePairList(IPairList):
|
||||||
"""
|
"""
|
||||||
return f"{self.name} - top {self._pairlistconfig['number_assets']} volume pairs."
|
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]:
|
def gen_pairlist(self, tickers: Tickers) -> List[str]:
|
||||||
"""
|
"""
|
||||||
Generate the pairlist
|
Generate the pairlist
|
||||||
|
|
|
@ -13,7 +13,7 @@ from freqtrade.constants import Config, ListPairsWithTimeframes
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.exchange.types import Tickers
|
from freqtrade.exchange.types import Tickers
|
||||||
from freqtrade.misc import plural
|
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
|
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"{self._min_rate_of_change}{max_rate_desc} over the "
|
||||||
f"last {plural(self._days, 'day')}.")
|
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]:
|
def filter_pairlist(self, pairlist: List[str], tickers: Tickers) -> List[str]:
|
||||||
"""
|
"""
|
||||||
Validate trading range
|
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 pydantic import BaseModel
|
||||||
|
|
||||||
from freqtrade.constants import DATETIME_PRINT_FORMAT, IntOrInf
|
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
|
from freqtrade.types import ValidExchangesType
|
||||||
|
|
||||||
|
|
||||||
|
class ExchangeModePayloadMixin(BaseModel):
|
||||||
|
trading_mode: Optional[TradingMode]
|
||||||
|
margin_mode: Optional[MarginMode]
|
||||||
|
exchange: Optional[str]
|
||||||
|
|
||||||
|
|
||||||
class Ping(BaseModel):
|
class Ping(BaseModel):
|
||||||
status: str
|
status: str
|
||||||
|
|
||||||
|
@ -28,6 +34,23 @@ class StatusMsg(BaseModel):
|
||||||
status: str
|
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):
|
class ResultMsg(BaseModel):
|
||||||
result: str
|
result: str
|
||||||
|
|
||||||
|
@ -377,6 +400,10 @@ class WhitelistResponse(BaseModel):
|
||||||
method: List[str]
|
method: List[str]
|
||||||
|
|
||||||
|
|
||||||
|
class WhitelistEvaluateResponse(BackgroundTaskResult):
|
||||||
|
result: Optional[WhitelistResponse]
|
||||||
|
|
||||||
|
|
||||||
class DeleteTrade(BaseModel):
|
class DeleteTrade(BaseModel):
|
||||||
cancel_order_count: int
|
cancel_order_count: int
|
||||||
result: str
|
result: str
|
||||||
|
@ -401,6 +428,23 @@ class ExchangeListResponse(BaseModel):
|
||||||
exchanges: List[ValidExchangesType]
|
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):
|
class FreqAIModelListResponse(BaseModel):
|
||||||
freqaimodels: List[str]
|
freqaimodels: List[str]
|
||||||
|
|
||||||
|
|
|
@ -48,7 +48,8 @@ logger = logging.getLogger(__name__)
|
||||||
# 2.27: Add /trades/<id>/reload endpoint
|
# 2.27: Add /trades/<id>/reload endpoint
|
||||||
# 2.28: Switch reload endpoint to Post
|
# 2.28: Switch reload endpoint to Post
|
||||||
# 2.29: Add /exchanges endpoint
|
# 2.29: Add /exchanges endpoint
|
||||||
API_VERSION = 2.29
|
# 2.30: new /pairlists endpoint
|
||||||
|
API_VERSION = 2.30
|
||||||
|
|
||||||
# Public API, requires no auth.
|
# Public API, requires no auth.
|
||||||
router_public = APIRouter()
|
router_public = APIRouter()
|
||||||
|
|
|
@ -3,6 +3,7 @@ from uuid import uuid4
|
||||||
|
|
||||||
from fastapi import Depends, HTTPException
|
from fastapi import Depends, HTTPException
|
||||||
|
|
||||||
|
from freqtrade.constants import Config
|
||||||
from freqtrade.enums import RunMode
|
from freqtrade.enums import RunMode
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
from freqtrade.persistence.models import _request_id_ctx_var
|
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']
|
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)):
|
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
|
from freqtrade.resolvers import ExchangeResolver
|
||||||
ApiBG.exchange = ExchangeResolver.load_exchange(
|
exchange = ExchangeResolver.load_exchange(
|
||||||
config, load_leverage_tiers=False)
|
config, load_leverage_tiers=False)
|
||||||
return ApiBG.exchange
|
ApiBG.exchanges[exchange_key] = exchange
|
||||||
|
return exchange
|
||||||
|
|
||||||
|
|
||||||
def get_message_stream():
|
def get_message_stream():
|
||||||
|
|
|
@ -114,6 +114,7 @@ class ApiServer(RPCHandler):
|
||||||
|
|
||||||
def configure_app(self, app: FastAPI, config):
|
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_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_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 as api_v1
|
||||||
from freqtrade.rpc.api_server.api_v1 import router_public as api_v1_public
|
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),
|
dependencies=[Depends(http_basic_or_jwt_token),
|
||||||
Depends(is_webserver_mode)],
|
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(ws_router, prefix="/api/v1")
|
||||||
app.include_router(router_login, prefix="/api/v1", tags=["auth"])
|
app.include_router(router_login, prefix="/api/v1", tags=["auth"])
|
||||||
# UI Router MUST be last!
|
# 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():
|
class ApiBG():
|
||||||
|
@ -13,4 +25,15 @@ class ApiBG():
|
||||||
}
|
}
|
||||||
bgtask_running: bool = False
|
bgtask_running: bool = False
|
||||||
# Exchange - only available in webserver mode.
|
# 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}")
|
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):
|
def plot_config(self):
|
||||||
"""Return plot configuration if the strategy defines one.
|
"""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")
|
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):
|
def test_merge_ft_has_dict(default_conf, mocker):
|
||||||
mocker.patch.multiple(EXMS,
|
mocker.patch.multiple(EXMS,
|
||||||
_init_ccxt=MagicMock(return_value=MagicMock()),
|
_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):
|
def test_list_available_pairs(botclient):
|
||||||
ftbot, client = botclient
|
ftbot, client = botclient
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user