Merge branch 'freqtrade:develop' into develop

This commit is contained in:
hippocritical 2023-06-11 22:55:03 +02:00 committed by GitHub
commit d748cf6531
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 799 additions and 30 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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(

View File

@ -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

View File

@ -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:

View File

@ -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.

View File

@ -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.

View File

@ -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

View File

@ -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.

View File

@ -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)

View File

@ -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', [])

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View 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'],
}

View File

@ -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]

View File

@ -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()

View File

@ -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():

View File

@ -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!

View File

@ -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())

View File

@ -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.

View File

@ -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()),

View File

@ -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