mirror of
https://github.com/freqtrade/freqtrade.git
synced 2024-11-10 02:12:01 +00:00
Merge remote-tracking branch 'origin/develop' into use-datasieve
This commit is contained in:
commit
447feb16b4
1
.github/workflows/ci.yml
vendored
1
.github/workflows/ci.yml
vendored
|
@ -136,6 +136,7 @@ jobs:
|
|||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
|
||||
- name: Cache_dependencies
|
||||
uses: actions/cache@v3
|
||||
|
|
|
@ -18,7 +18,7 @@ repos:
|
|||
- types-requests==2.31.0.1
|
||||
- types-tabulate==0.9.0.2
|
||||
- types-python-dateutil==2.8.19.13
|
||||
- SQLAlchemy==2.0.15
|
||||
- SQLAlchemy==2.0.16
|
||||
# stages: [push]
|
||||
|
||||
- repo: https://github.com/pycqa/isort
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
FROM python:3.10.11-slim-bullseye as base
|
||||
FROM python:3.11.4-slim-bullseye as base
|
||||
|
||||
# Setup env
|
||||
ENV LANG C.UTF-8
|
||||
|
|
|
@ -43,10 +43,10 @@ The FreqAI strategy requires including the following lines of code in the standa
|
|||
|
||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
|
||||
# the model will return all labels created by user in `set_freqai_labels()`
|
||||
# the model will return all labels created by user in `set_freqai_targets()`
|
||||
# (& appended targets), an indication of whether or not the prediction should be accepted,
|
||||
# the target mean/std values for each of the labels created by user in
|
||||
# `feature_engineering_*` for each training period.
|
||||
# `set_freqai_targets()` for each training period.
|
||||
|
||||
dataframe = self.freqai.start(dataframe, metadata, self)
|
||||
|
||||
|
|
|
@ -180,6 +180,9 @@ You can ask for each of the defined features to be included also for informative
|
|||
|
||||
In total, the number of features the user of the presented example strat has created is: length of `include_timeframes` * no. features in `feature_engineering_expand_*()` * length of `include_corr_pairlist` * no. `include_shifted_candles` * length of `indicator_periods_candles`
|
||||
$= 3 * 3 * 3 * 2 * 2 = 108$.
|
||||
|
||||
!!! note "Learn more about creative feature engineering"
|
||||
Check out our [medium article](https://emergentmethods.medium.com/freqai-from-price-to-prediction-6fadac18b665) geared toward helping users learn how to creatively engineer features.
|
||||
|
||||
### Gain finer control over `feature_engineering_*` functions with `metadata`
|
||||
|
||||
|
|
|
@ -107,6 +107,13 @@ This is for performance reasons - FreqAI relies on making quick predictions/retr
|
|||
it needs to download all the training data at the beginning of a dry/live instance. FreqAI stores and appends
|
||||
new candles automatically for future retrains. This means that if new pairs arrive later in the dry run due to a volume pairlist, it will not have the data ready. However, FreqAI does work with the `ShufflePairlist` or a `VolumePairlist` which keeps the total pairlist constant (but reorders the pairs according to volume).
|
||||
|
||||
## Additional learning materials
|
||||
|
||||
Here we compile some external materials that provide deeper looks into various components of FreqAI:
|
||||
|
||||
- [Real-time head-to-head: Adaptive modeling of financial market data using XGBoost and CatBoost](https://emergentmethods.medium.com/real-time-head-to-head-adaptive-modeling-of-financial-market-data-using-xgboost-and-catboost-995a115a7495)
|
||||
- [FreqAI - from price to prediction](https://emergentmethods.medium.com/freqai-from-price-to-prediction-6fadac18b665)
|
||||
|
||||
## Credits
|
||||
|
||||
FreqAI is developed by a group of individuals who all contribute specific skillsets to the project.
|
||||
|
|
|
@ -7,7 +7,7 @@ from typing import Dict, List, Optional, Tuple
|
|||
from pandas import DataFrame, concat
|
||||
|
||||
from freqtrade.configuration import TimeRange
|
||||
from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS
|
||||
from freqtrade.constants import DATETIME_PRINT_FORMAT, DEFAULT_DATAFRAME_COLUMNS
|
||||
from freqtrade.data.converter import (clean_ohlcv_dataframe, ohlcv_to_dataframe,
|
||||
trades_remove_duplicates, trades_to_ohlcv)
|
||||
from freqtrade.data.history.idatahandler import IDataHandler, get_datahandler
|
||||
|
@ -227,9 +227,11 @@ def _download_pair_history(pair: str, *,
|
|||
)
|
||||
|
||||
logger.debug("Current Start: %s",
|
||||
f"{data.iloc[0]['date']:DATETIME_PRINT_FORMAT}" if not data.empty else 'None')
|
||||
f"{data.iloc[0]['date']:{DATETIME_PRINT_FORMAT}}"
|
||||
if not data.empty else 'None')
|
||||
logger.debug("Current End: %s",
|
||||
f"{data.iloc[-1]['date']:DATETIME_PRINT_FORMAT}" if not data.empty else 'None')
|
||||
f"{data.iloc[-1]['date']:{DATETIME_PRINT_FORMAT}}"
|
||||
if not data.empty else 'None')
|
||||
|
||||
# Default since_ms to 30 days if nothing is given
|
||||
new_data = exchange.get_historic_ohlcv(pair=pair,
|
||||
|
@ -252,10 +254,12 @@ def _download_pair_history(pair: str, *,
|
|||
data = clean_ohlcv_dataframe(concat([data, new_dataframe], axis=0), timeframe, pair,
|
||||
fill_missing=False, drop_incomplete=False)
|
||||
|
||||
logger.debug("New Start: %s",
|
||||
f"{data.iloc[0]['date']:DATETIME_PRINT_FORMAT}" if not data.empty else 'None')
|
||||
logger.debug("New Start: %s",
|
||||
f"{data.iloc[0]['date']:{DATETIME_PRINT_FORMAT}}"
|
||||
if not data.empty else 'None')
|
||||
logger.debug("New End: %s",
|
||||
f"{data.iloc[-1]['date']:DATETIME_PRINT_FORMAT}" if not data.empty else 'None')
|
||||
f"{data.iloc[-1]['date']:{DATETIME_PRINT_FORMAT}}"
|
||||
if not data.empty else 'None')
|
||||
|
||||
data_handler.ohlcv_store(pair, timeframe, data=data, candle_type=candle_type)
|
||||
return True
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from enum import Enum
|
||||
|
||||
|
||||
class MarginMode(Enum):
|
||||
class MarginMode(str, Enum):
|
||||
"""
|
||||
Enum to distinguish between
|
||||
cross margin/futures margin_mode and
|
||||
|
|
|
@ -1148,8 +1148,8 @@ class Exchange:
|
|||
else:
|
||||
limit_rate = stop_price * (2 - limit_price_pct)
|
||||
|
||||
bad_stop_price = ((stop_price <= limit_rate) if side ==
|
||||
"sell" else (stop_price >= limit_rate))
|
||||
bad_stop_price = ((stop_price < limit_rate) if side ==
|
||||
"sell" else (stop_price > limit_rate))
|
||||
# Ensure rate is less than stop price
|
||||
if bad_stop_price:
|
||||
# This can for example happen if the stop / liquidation price is set to 0
|
||||
|
|
|
@ -125,6 +125,20 @@ class Okx(Exchange):
|
|||
params['posSide'] = self._get_posSide(side, reduceOnly)
|
||||
return params
|
||||
|
||||
def __fetch_leverage_already_set(self, pair: str, leverage: float, side: BuySell) -> bool:
|
||||
try:
|
||||
res_lev = self._api.fetch_leverage(symbol=pair, params={
|
||||
"mgnMode": self.margin_mode.value,
|
||||
"posSide": self._get_posSide(side, False),
|
||||
})
|
||||
self._log_exchange_response('get_leverage', res_lev)
|
||||
already_set = all(float(x['lever']) == leverage for x in res_lev['data'])
|
||||
return already_set
|
||||
|
||||
except ccxt.BaseError:
|
||||
# Assume all errors as "not set yet"
|
||||
return False
|
||||
|
||||
@retrier
|
||||
def _lev_prep(self, pair: str, leverage: float, side: BuySell, accept_fail: bool = False):
|
||||
if self.trading_mode != TradingMode.SPOT and self.margin_mode is not None:
|
||||
|
@ -141,8 +155,11 @@ class Okx(Exchange):
|
|||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e
|
||||
already_set = self.__fetch_leverage_already_set(pair, leverage, side)
|
||||
if not already_set:
|
||||
raise TemporaryError(
|
||||
f'Could not set leverage due to {e.__class__.__name__}. Message: {e}'
|
||||
) from e
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
|
@ -182,6 +199,7 @@ class Okx(Exchange):
|
|||
order_reg['type'] = 'stoploss'
|
||||
order_reg['status_stop'] = 'triggered'
|
||||
return order_reg
|
||||
order = self._order_contracts_to_amount(order)
|
||||
order['type'] = 'stoploss'
|
||||
return order
|
||||
|
||||
|
|
|
@ -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
|
||||
|
@ -635,7 +636,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(
|
||||
|
|
|
@ -5,6 +5,7 @@ from logging.handlers import RotatingFileHandler, SysLogHandler
|
|||
from freqtrade.constants import Config
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.loggers.buffering_handler import FTBufferingHandler
|
||||
from freqtrade.loggers.set_log_levels import set_loggers
|
||||
from freqtrade.loggers.std_err_stream_handler import FTStdErrStreamHandler
|
||||
|
||||
|
||||
|
@ -16,29 +17,6 @@ bufferHandler = FTBufferingHandler(1000)
|
|||
bufferHandler.setFormatter(Formatter(LOGFORMAT))
|
||||
|
||||
|
||||
def _set_loggers(verbosity: int = 0, api_verbosity: str = 'info') -> None:
|
||||
"""
|
||||
Set the logging level for third party libraries
|
||||
:return: None
|
||||
"""
|
||||
|
||||
logging.getLogger('requests').setLevel(
|
||||
logging.INFO if verbosity <= 1 else logging.DEBUG
|
||||
)
|
||||
logging.getLogger("urllib3").setLevel(
|
||||
logging.INFO if verbosity <= 1 else logging.DEBUG
|
||||
)
|
||||
logging.getLogger('ccxt.base.exchange').setLevel(
|
||||
logging.INFO if verbosity <= 2 else logging.DEBUG
|
||||
)
|
||||
logging.getLogger('telegram').setLevel(logging.INFO)
|
||||
logging.getLogger('httpx').setLevel(logging.WARNING)
|
||||
|
||||
logging.getLogger('werkzeug').setLevel(
|
||||
logging.ERROR if api_verbosity == 'error' else logging.INFO
|
||||
)
|
||||
|
||||
|
||||
def get_existing_handlers(handlertype):
|
||||
"""
|
||||
Returns Existing handler or None (if the handler has not yet been added to the root handlers).
|
||||
|
@ -115,6 +93,6 @@ def setup_logging(config: Config) -> None:
|
|||
logging.root.addHandler(handler_rf)
|
||||
|
||||
logging.root.setLevel(logging.INFO if verbosity < 1 else logging.DEBUG)
|
||||
_set_loggers(verbosity, config.get('api_server', {}).get('verbosity', 'info'))
|
||||
set_loggers(verbosity, config.get('api_server', {}).get('verbosity', 'info'))
|
||||
|
||||
logger.info('Verbosity set to %s', verbosity)
|
||||
|
|
25
freqtrade/loggers/set_log_levels.py
Normal file
25
freqtrade/loggers/set_log_levels.py
Normal file
|
@ -0,0 +1,25 @@
|
|||
|
||||
import logging
|
||||
|
||||
|
||||
def set_loggers(verbosity: int = 0, api_verbosity: str = 'info') -> None:
|
||||
"""
|
||||
Set the logging level for third party libraries
|
||||
:return: None
|
||||
"""
|
||||
|
||||
logging.getLogger('requests').setLevel(
|
||||
logging.INFO if verbosity <= 1 else logging.DEBUG
|
||||
)
|
||||
logging.getLogger("urllib3").setLevel(
|
||||
logging.INFO if verbosity <= 1 else logging.DEBUG
|
||||
)
|
||||
logging.getLogger('ccxt.base.exchange').setLevel(
|
||||
logging.INFO if verbosity <= 2 else logging.DEBUG
|
||||
)
|
||||
logging.getLogger('telegram').setLevel(logging.INFO)
|
||||
logging.getLogger('httpx').setLevel(logging.WARNING)
|
||||
|
||||
logging.getLogger('werkzeug').setLevel(
|
||||
logging.ERROR if api_verbosity == 'error' else logging.INFO
|
||||
)
|
|
@ -97,7 +97,7 @@ class Order(ModelBase):
|
|||
|
||||
@property
|
||||
def safe_filled(self) -> float:
|
||||
return self.filled if self.filled is not None else self.amount or 0.0
|
||||
return self.filled if self.filled is not None else 0.0
|
||||
|
||||
@property
|
||||
def safe_cost(self) -> float:
|
||||
|
@ -703,7 +703,7 @@ class LocalTrade():
|
|||
self.stoploss_order_id = None
|
||||
self.close_rate_requested = self.stop_loss
|
||||
self.exit_reason = ExitType.STOPLOSS_ON_EXCHANGE.value
|
||||
if self.is_open:
|
||||
if self.is_open and order.safe_filled > 0:
|
||||
logger.info(f'{order.order_type.upper()} is hit for {self}.')
|
||||
else:
|
||||
raise ValueError(f'Unknown order type: {order.order_type}')
|
||||
|
|
|
@ -12,7 +12,7 @@ from freqtrade.constants import Config, ListPairsWithTimeframes
|
|||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange.types import Tickers
|
||||
from freqtrade.misc import plural
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter
|
||||
from freqtrade.util import PeriodicCache, dt_floor_day, dt_now, dt_ts
|
||||
|
||||
|
||||
|
@ -68,6 +68,27 @@ class AgeFilter(IPairList):
|
|||
f"{self._max_days_listed} {plural(self._max_days_listed, 'day')}"
|
||||
) if self._max_days_listed else '')
|
||||
|
||||
@staticmethod
|
||||
def description() -> str:
|
||||
return "Filter pairs by age (days listed)."
|
||||
|
||||
@staticmethod
|
||||
def available_parameters() -> Dict[str, PairlistParameter]:
|
||||
return {
|
||||
"min_days_listed": {
|
||||
"type": "number",
|
||||
"default": 10,
|
||||
"description": "Minimum Days Listed",
|
||||
"help": "Minimum number of days a pair must have been listed on the exchange.",
|
||||
},
|
||||
"max_days_listed": {
|
||||
"type": "number",
|
||||
"default": None,
|
||||
"description": "Maximum Days Listed",
|
||||
"help": "Maximum number of days a pair must have been listed on the exchange.",
|
||||
},
|
||||
}
|
||||
|
||||
def filter_pairlist(self, pairlist: List[str], tickers: Tickers) -> List[str]:
|
||||
"""
|
||||
:param pairlist: pairlist to filter or sort
|
||||
|
|
|
@ -4,7 +4,7 @@ PairList Handler base class
|
|||
import logging
|
||||
from abc import ABC, abstractmethod, abstractproperty
|
||||
from copy import deepcopy
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Any, Dict, List, Literal, Optional, TypedDict, Union
|
||||
|
||||
from freqtrade.constants import Config
|
||||
from freqtrade.exceptions import OperationalException
|
||||
|
@ -16,8 +16,44 @@ from freqtrade.mixins import LoggingMixin
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class __PairlistParameterBase(TypedDict):
|
||||
description: str
|
||||
help: str
|
||||
|
||||
|
||||
class __NumberPairlistParameter(__PairlistParameterBase):
|
||||
type: Literal["number"]
|
||||
default: Union[int, float, None]
|
||||
|
||||
|
||||
class __StringPairlistParameter(__PairlistParameterBase):
|
||||
type: Literal["string"]
|
||||
default: Union[str, None]
|
||||
|
||||
|
||||
class __OptionPairlistParameter(__PairlistParameterBase):
|
||||
type: Literal["option"]
|
||||
default: Union[str, None]
|
||||
options: List[str]
|
||||
|
||||
|
||||
class __BoolPairlistParameter(__PairlistParameterBase):
|
||||
type: Literal["boolean"]
|
||||
default: Union[bool, None]
|
||||
|
||||
|
||||
PairlistParameter = Union[
|
||||
__NumberPairlistParameter,
|
||||
__StringPairlistParameter,
|
||||
__OptionPairlistParameter,
|
||||
__BoolPairlistParameter
|
||||
]
|
||||
|
||||
|
||||
class IPairList(LoggingMixin, ABC):
|
||||
|
||||
is_pairlist_generator = False
|
||||
|
||||
def __init__(self, exchange: Exchange, pairlistmanager,
|
||||
config: Config, pairlistconfig: Dict[str, Any],
|
||||
pairlist_pos: int) -> None:
|
||||
|
@ -53,6 +89,37 @@ class IPairList(LoggingMixin, ABC):
|
|||
If no Pairlist requires tickers, an empty Dict is passed
|
||||
as tickers argument to filter_pairlist
|
||||
"""
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def description() -> str:
|
||||
"""
|
||||
Return description of this Pairlist Handler
|
||||
-> Please overwrite in subclasses
|
||||
"""
|
||||
return ""
|
||||
|
||||
@staticmethod
|
||||
def available_parameters() -> Dict[str, PairlistParameter]:
|
||||
"""
|
||||
Return parameters used by this Pairlist Handler, and their type
|
||||
contains a dictionary with the parameter name as key, and a dictionary
|
||||
with the type and default value.
|
||||
-> Please overwrite in subclasses
|
||||
"""
|
||||
return {}
|
||||
|
||||
@staticmethod
|
||||
def refresh_period_parameter() -> Dict[str, PairlistParameter]:
|
||||
return {
|
||||
"refresh_period": {
|
||||
"type": "number",
|
||||
"default": 1800,
|
||||
"description": "Refresh period",
|
||||
"help": "Refresh period in seconds",
|
||||
}
|
||||
}
|
||||
|
||||
@abstractmethod
|
||||
def short_desc(self) -> str:
|
||||
|
|
|
@ -7,7 +7,7 @@ from typing import Any, Dict, List
|
|||
from freqtrade.constants import Config
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange.types import Tickers
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -43,6 +43,27 @@ class OffsetFilter(IPairList):
|
|||
return f"{self.name} - Taking {self._number_pairs} Pairs, starting from {self._offset}."
|
||||
return f"{self.name} - Offsetting pairs by {self._offset}."
|
||||
|
||||
@staticmethod
|
||||
def description() -> str:
|
||||
return "Offset pair list filter."
|
||||
|
||||
@staticmethod
|
||||
def available_parameters() -> Dict[str, PairlistParameter]:
|
||||
return {
|
||||
"offset": {
|
||||
"type": "number",
|
||||
"default": 0,
|
||||
"description": "Offset",
|
||||
"help": "Offset of the pairlist.",
|
||||
},
|
||||
"number_assets": {
|
||||
"type": "number",
|
||||
"default": 0,
|
||||
"description": "Number of assets",
|
||||
"help": "Number of assets to use from the pairlist, starting from offset.",
|
||||
},
|
||||
}
|
||||
|
||||
def filter_pairlist(self, pairlist: List[str], tickers: Tickers) -> List[str]:
|
||||
"""
|
||||
Filters and sorts pairlist and returns the whitelist again.
|
||||
|
|
|
@ -9,7 +9,7 @@ import pandas as pd
|
|||
from freqtrade.constants import Config
|
||||
from freqtrade.exchange.types import Tickers
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -40,6 +40,27 @@ class PerformanceFilter(IPairList):
|
|||
"""
|
||||
return f"{self.name} - Sorting pairs by performance."
|
||||
|
||||
@staticmethod
|
||||
def description() -> str:
|
||||
return "Filter pairs by performance."
|
||||
|
||||
@staticmethod
|
||||
def available_parameters() -> Dict[str, PairlistParameter]:
|
||||
return {
|
||||
"minutes": {
|
||||
"type": "number",
|
||||
"default": 0,
|
||||
"description": "Minutes",
|
||||
"help": "Consider trades from the last X minutes. 0 means all trades.",
|
||||
},
|
||||
"min_profit": {
|
||||
"type": "number",
|
||||
"default": None,
|
||||
"description": "Minimum profit",
|
||||
"help": "Minimum profit in percent. Pairs with less profit are removed.",
|
||||
},
|
||||
}
|
||||
|
||||
def filter_pairlist(self, pairlist: List[str], tickers: Tickers) -> List[str]:
|
||||
"""
|
||||
Filters and sorts pairlist and returns the allowlist again.
|
||||
|
|
|
@ -46,6 +46,10 @@ class PrecisionFilter(IPairList):
|
|||
"""
|
||||
return f"{self.name} - Filtering untradable pairs."
|
||||
|
||||
@staticmethod
|
||||
def description() -> str:
|
||||
return "Filters low-value coins which would not allow setting stoplosses."
|
||||
|
||||
def _validate_pair(self, pair: str, ticker: Optional[Ticker]) -> bool:
|
||||
"""
|
||||
Check if pair has enough room to add a stoploss to avoid "unsellable" buys of very
|
||||
|
|
|
@ -7,7 +7,7 @@ from typing import Any, Dict, Optional
|
|||
from freqtrade.constants import Config
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange.types import Ticker
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -65,6 +65,40 @@ class PriceFilter(IPairList):
|
|||
|
||||
return f"{self.name} - No price filters configured."
|
||||
|
||||
@staticmethod
|
||||
def description() -> str:
|
||||
return "Filter pairs by price."
|
||||
|
||||
@staticmethod
|
||||
def available_parameters() -> Dict[str, PairlistParameter]:
|
||||
return {
|
||||
"low_price_ratio": {
|
||||
"type": "number",
|
||||
"default": 0,
|
||||
"description": "Low price ratio",
|
||||
"help": ("Remove pairs where a price move of 1 price unit (pip) "
|
||||
"is above this ratio."),
|
||||
},
|
||||
"min_price": {
|
||||
"type": "number",
|
||||
"default": 0,
|
||||
"description": "Minimum price",
|
||||
"help": "Remove pairs with a price below this value.",
|
||||
},
|
||||
"max_price": {
|
||||
"type": "number",
|
||||
"default": 0,
|
||||
"description": "Maximum price",
|
||||
"help": "Remove pairs with a price above this value.",
|
||||
},
|
||||
"max_value": {
|
||||
"type": "number",
|
||||
"default": 0,
|
||||
"description": "Maximum value",
|
||||
"help": "Remove pairs with a value (price * amount) above this value.",
|
||||
},
|
||||
}
|
||||
|
||||
def _validate_pair(self, pair: str, ticker: Optional[Ticker]) -> bool:
|
||||
"""
|
||||
Check if if one price-step (pip) is > than a certain barrier.
|
||||
|
|
|
@ -8,7 +8,7 @@ from typing import Any, Dict, List, Optional
|
|||
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange.types import Tickers
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -28,6 +28,7 @@ class ProducerPairList(IPairList):
|
|||
}
|
||||
],
|
||||
"""
|
||||
is_pairlist_generator = True
|
||||
|
||||
def __init__(self, exchange, pairlistmanager,
|
||||
config: Dict[str, Any], pairlistconfig: Dict[str, Any],
|
||||
|
@ -56,6 +57,28 @@ class ProducerPairList(IPairList):
|
|||
"""
|
||||
return f"{self.name} - {self._producer_name}"
|
||||
|
||||
@staticmethod
|
||||
def description() -> str:
|
||||
return "Get a pairlist from an upstream bot."
|
||||
|
||||
@staticmethod
|
||||
def available_parameters() -> Dict[str, PairlistParameter]:
|
||||
return {
|
||||
"number_assets": {
|
||||
"type": "number",
|
||||
"default": 0,
|
||||
"description": "Number of assets",
|
||||
"help": "Number of assets to use from the pairlist",
|
||||
},
|
||||
"producer_name": {
|
||||
"type": "string",
|
||||
"default": "default",
|
||||
"description": "Producer name",
|
||||
"help": ("Name of the producer to use. Requires additional "
|
||||
"external_message_consumer configuration.")
|
||||
},
|
||||
}
|
||||
|
||||
def _filter_pairlist(self, pairlist: Optional[List[str]]):
|
||||
upstream_pairlist = self._pairlistmanager._dataprovider.get_producer_pairs(
|
||||
self._producer_name)
|
||||
|
|
|
@ -15,7 +15,7 @@ from freqtrade import __version__
|
|||
from freqtrade.constants import Config
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange.types import Tickers
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -23,6 +23,8 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
class RemotePairList(IPairList):
|
||||
|
||||
is_pairlist_generator = True
|
||||
|
||||
def __init__(self, exchange, pairlistmanager,
|
||||
config: Config, pairlistconfig: Dict[str, Any],
|
||||
pairlist_pos: int) -> None:
|
||||
|
@ -63,6 +65,46 @@ class RemotePairList(IPairList):
|
|||
"""
|
||||
return f"{self.name} - {self._pairlistconfig['number_assets']} pairs from RemotePairlist."
|
||||
|
||||
@staticmethod
|
||||
def description() -> str:
|
||||
return "Retrieve pairs from a remote API."
|
||||
|
||||
@staticmethod
|
||||
def available_parameters() -> Dict[str, PairlistParameter]:
|
||||
return {
|
||||
"number_assets": {
|
||||
"type": "number",
|
||||
"default": 0,
|
||||
"description": "Number of assets",
|
||||
"help": "Number of assets to use from the pairlist.",
|
||||
},
|
||||
"pairlist_url": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "URL to fetch pairlist from",
|
||||
"help": "URL to fetch pairlist from",
|
||||
},
|
||||
**IPairList.refresh_period_parameter(),
|
||||
"keep_pairlist_on_failure": {
|
||||
"type": "boolean",
|
||||
"default": True,
|
||||
"description": "Keep last pairlist on failure",
|
||||
"help": "Keep last pairlist on failure",
|
||||
},
|
||||
"read_timeout": {
|
||||
"type": "number",
|
||||
"default": 60,
|
||||
"description": "Read timeout",
|
||||
"help": "Request timeout for remote pairlist",
|
||||
},
|
||||
"bearer_token": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "Bearer token",
|
||||
"help": "Bearer token - used for auth against the upstream service.",
|
||||
},
|
||||
}
|
||||
|
||||
def process_json(self, jsonparse) -> List[str]:
|
||||
|
||||
pairlist = jsonparse.get('pairs', [])
|
||||
|
|
|
@ -9,7 +9,7 @@ from freqtrade.constants import Config
|
|||
from freqtrade.enums import RunMode
|
||||
from freqtrade.exchange import timeframe_to_seconds
|
||||
from freqtrade.exchange.types import Tickers
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter
|
||||
from freqtrade.util.periodic_cache import PeriodicCache
|
||||
|
||||
|
||||
|
@ -55,6 +55,28 @@ class ShuffleFilter(IPairList):
|
|||
return (f"{self.name} - Shuffling pairs every {self._shuffle_freq}" +
|
||||
(f", seed = {self._seed}." if self._seed is not None else "."))
|
||||
|
||||
@staticmethod
|
||||
def description() -> str:
|
||||
return "Randomize pairlist order."
|
||||
|
||||
@staticmethod
|
||||
def available_parameters() -> Dict[str, PairlistParameter]:
|
||||
return {
|
||||
"shuffle_frequency": {
|
||||
"type": "option",
|
||||
"default": "candle",
|
||||
"options": ["candle", "iteration"],
|
||||
"description": "Shuffle frequency",
|
||||
"help": "Shuffle frequency. Can be either 'candle' or 'iteration'.",
|
||||
},
|
||||
"seed": {
|
||||
"type": "number",
|
||||
"default": None,
|
||||
"description": "Random Seed",
|
||||
"help": "Seed for random number generator. Not used in live mode.",
|
||||
},
|
||||
}
|
||||
|
||||
def filter_pairlist(self, pairlist: List[str], tickers: Tickers) -> List[str]:
|
||||
"""
|
||||
Filters and sorts pairlist and returns the whitelist again.
|
||||
|
|
|
@ -7,7 +7,7 @@ from typing import Any, Dict, Optional
|
|||
from freqtrade.constants import Config
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange.types import Ticker
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -45,6 +45,21 @@ class SpreadFilter(IPairList):
|
|||
return (f"{self.name} - Filtering pairs with ask/bid diff above "
|
||||
f"{self._max_spread_ratio:.2%}.")
|
||||
|
||||
@staticmethod
|
||||
def description() -> str:
|
||||
return "Filter by bid/ask difference."
|
||||
|
||||
@staticmethod
|
||||
def available_parameters() -> Dict[str, PairlistParameter]:
|
||||
return {
|
||||
"max_spread_ratio": {
|
||||
"type": "number",
|
||||
"default": 0.005,
|
||||
"description": "Max spread ratio",
|
||||
"help": "Max spread ratio for a pair to be considered.",
|
||||
},
|
||||
}
|
||||
|
||||
def _validate_pair(self, pair: str, ticker: Optional[Ticker]) -> bool:
|
||||
"""
|
||||
Validate spread for the ticker
|
||||
|
|
|
@ -9,7 +9,7 @@ from typing import Any, Dict, List
|
|||
|
||||
from freqtrade.constants import Config
|
||||
from freqtrade.exchange.types import Tickers
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -17,6 +17,8 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
class StaticPairList(IPairList):
|
||||
|
||||
is_pairlist_generator = True
|
||||
|
||||
def __init__(self, exchange, pairlistmanager,
|
||||
config: Config, pairlistconfig: Dict[str, Any],
|
||||
pairlist_pos: int) -> None:
|
||||
|
@ -40,6 +42,21 @@ class StaticPairList(IPairList):
|
|||
"""
|
||||
return f"{self.name}"
|
||||
|
||||
@staticmethod
|
||||
def description() -> str:
|
||||
return "Use pairlist as configured in config."
|
||||
|
||||
@staticmethod
|
||||
def available_parameters() -> Dict[str, PairlistParameter]:
|
||||
return {
|
||||
"allow_inactive": {
|
||||
"type": "boolean",
|
||||
"default": False,
|
||||
"description": "Allow inactive pairs",
|
||||
"help": "Allow inactive pairs to be in the whitelist.",
|
||||
},
|
||||
}
|
||||
|
||||
def gen_pairlist(self, tickers: Tickers) -> List[str]:
|
||||
"""
|
||||
Generate the pairlist
|
||||
|
|
|
@ -15,7 +15,7 @@ from freqtrade.constants import Config, ListPairsWithTimeframes
|
|||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange.types import Tickers
|
||||
from freqtrade.misc import plural
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter
|
||||
from freqtrade.util import dt_floor_day, dt_now, dt_ts
|
||||
|
||||
|
||||
|
@ -64,6 +64,34 @@ class VolatilityFilter(IPairList):
|
|||
f"{self._min_volatility}-{self._max_volatility} "
|
||||
f" the last {self._days} {plural(self._days, 'day')}.")
|
||||
|
||||
@staticmethod
|
||||
def description() -> str:
|
||||
return "Filter pairs by their recent volatility."
|
||||
|
||||
@staticmethod
|
||||
def available_parameters() -> Dict[str, PairlistParameter]:
|
||||
return {
|
||||
"lookback_days": {
|
||||
"type": "number",
|
||||
"default": 10,
|
||||
"description": "Lookback Days",
|
||||
"help": "Number of days to look back at.",
|
||||
},
|
||||
"min_volatility": {
|
||||
"type": "number",
|
||||
"default": 0,
|
||||
"description": "Minimum Volatility",
|
||||
"help": "Minimum volatility a pair must have to be considered.",
|
||||
},
|
||||
"max_volatility": {
|
||||
"type": "number",
|
||||
"default": None,
|
||||
"description": "Maximum Volatility",
|
||||
"help": "Maximum volatility a pair must have to be considered.",
|
||||
},
|
||||
**IPairList.refresh_period_parameter()
|
||||
}
|
||||
|
||||
def filter_pairlist(self, pairlist: List[str], tickers: Tickers) -> List[str]:
|
||||
"""
|
||||
Validate trading range
|
||||
|
|
|
@ -14,7 +14,7 @@ from freqtrade.exceptions import OperationalException
|
|||
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_prev_date
|
||||
from freqtrade.exchange.types import Tickers
|
||||
from freqtrade.misc import format_ms_time
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter
|
||||
from freqtrade.util import dt_now
|
||||
|
||||
|
||||
|
@ -26,6 +26,8 @@ SORT_VALUES = ['quoteVolume']
|
|||
|
||||
class VolumePairList(IPairList):
|
||||
|
||||
is_pairlist_generator = True
|
||||
|
||||
def __init__(self, exchange, pairlistmanager,
|
||||
config: Config, pairlistconfig: Dict[str, Any],
|
||||
pairlist_pos: int) -> None:
|
||||
|
@ -112,6 +114,53 @@ class VolumePairList(IPairList):
|
|||
"""
|
||||
return f"{self.name} - top {self._pairlistconfig['number_assets']} volume pairs."
|
||||
|
||||
@staticmethod
|
||||
def description() -> str:
|
||||
return "Provides dynamic pair list based on trade volumes."
|
||||
|
||||
@staticmethod
|
||||
def available_parameters() -> Dict[str, PairlistParameter]:
|
||||
return {
|
||||
"number_assets": {
|
||||
"type": "number",
|
||||
"default": 30,
|
||||
"description": "Number of assets",
|
||||
"help": "Number of assets to use from the pairlist",
|
||||
},
|
||||
"sort_key": {
|
||||
"type": "option",
|
||||
"default": "quoteVolume",
|
||||
"options": SORT_VALUES,
|
||||
"description": "Sort key",
|
||||
"help": "Sort key to use for sorting the pairlist.",
|
||||
},
|
||||
"min_value": {
|
||||
"type": "number",
|
||||
"default": 0,
|
||||
"description": "Minimum value",
|
||||
"help": "Minimum value to use for filtering the pairlist.",
|
||||
},
|
||||
**IPairList.refresh_period_parameter(),
|
||||
"lookback_days": {
|
||||
"type": "number",
|
||||
"default": 0,
|
||||
"description": "Lookback Days",
|
||||
"help": "Number of days to look back at.",
|
||||
},
|
||||
"lookback_timeframe": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "Lookback Timeframe",
|
||||
"help": "Timeframe to use for lookback.",
|
||||
},
|
||||
"lookback_period": {
|
||||
"type": "number",
|
||||
"default": 0,
|
||||
"description": "Lookback Period",
|
||||
"help": "Number of periods to look back at.",
|
||||
},
|
||||
}
|
||||
|
||||
def gen_pairlist(self, tickers: Tickers) -> List[str]:
|
||||
"""
|
||||
Generate the pairlist
|
||||
|
|
|
@ -13,7 +13,7 @@ from freqtrade.constants import Config, ListPairsWithTimeframes
|
|||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange.types import Tickers
|
||||
from freqtrade.misc import plural
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter
|
||||
from freqtrade.util import dt_floor_day, dt_now, dt_ts
|
||||
|
||||
|
||||
|
@ -62,6 +62,34 @@ class RangeStabilityFilter(IPairList):
|
|||
f"{self._min_rate_of_change}{max_rate_desc} over the "
|
||||
f"last {plural(self._days, 'day')}.")
|
||||
|
||||
@staticmethod
|
||||
def description() -> str:
|
||||
return "Filters pairs by their rate of change."
|
||||
|
||||
@staticmethod
|
||||
def available_parameters() -> Dict[str, PairlistParameter]:
|
||||
return {
|
||||
"lookback_days": {
|
||||
"type": "number",
|
||||
"default": 10,
|
||||
"description": "Lookback Days",
|
||||
"help": "Number of days to look back at.",
|
||||
},
|
||||
"min_rate_of_change": {
|
||||
"type": "number",
|
||||
"default": 0.01,
|
||||
"description": "Minimum Rate of Change",
|
||||
"help": "Minimum rate of change to filter pairs.",
|
||||
},
|
||||
"max_rate_of_change": {
|
||||
"type": "number",
|
||||
"default": None,
|
||||
"description": "Maximum Rate of Change",
|
||||
"help": "Maximum rate of change to filter pairs.",
|
||||
},
|
||||
**IPairList.refresh_period_parameter()
|
||||
}
|
||||
|
||||
def filter_pairlist(self, pairlist: List[str], tickers: Tickers) -> List[str]:
|
||||
"""
|
||||
Validate trading range
|
||||
|
|
145
freqtrade/rpc/api_server/api_background_tasks.py
Normal file
145
freqtrade/rpc/api_server/api_background_tasks.py
Normal file
|
@ -0,0 +1,145 @@
|
|||
import logging
|
||||
from copy import deepcopy
|
||||
|
||||
from fastapi import APIRouter, BackgroundTasks, Depends
|
||||
from fastapi.exceptions import HTTPException
|
||||
|
||||
from freqtrade.constants import Config
|
||||
from freqtrade.enums import CandleType
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.rpc.api_server.api_schemas import (BackgroundTaskStatus, BgJobStarted,
|
||||
ExchangeModePayloadMixin, PairListsPayload,
|
||||
PairListsResponse, WhitelistEvaluateResponse)
|
||||
from freqtrade.rpc.api_server.deps import get_config, get_exchange
|
||||
from freqtrade.rpc.api_server.webserver_bgwork import ApiBG
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Private API, protected by authentication and webserver_mode dependency
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get('/background/{jobid}', response_model=BackgroundTaskStatus, tags=['webserver'])
|
||||
def background_job(jobid: str):
|
||||
if not (job := ApiBG.jobs.get(jobid)):
|
||||
raise HTTPException(status_code=404, detail='Job not found.')
|
||||
|
||||
return {
|
||||
'job_id': jobid,
|
||||
'job_category': job['category'],
|
||||
'status': job['status'],
|
||||
'running': job['is_running'],
|
||||
'progress': job.get('progress'),
|
||||
# 'job_error': job['error'],
|
||||
}
|
||||
|
||||
|
||||
@router.get('/pairlists/available',
|
||||
response_model=PairListsResponse, tags=['pairlists', 'webserver'])
|
||||
def list_pairlists(config=Depends(get_config)):
|
||||
from freqtrade.resolvers import PairListResolver
|
||||
pairlists = PairListResolver.search_all_objects(
|
||||
config, False)
|
||||
pairlists = sorted(pairlists, key=lambda x: x['name'])
|
||||
|
||||
return {'pairlists': [{
|
||||
"name": x['name'],
|
||||
"is_pairlist_generator": x['class'].is_pairlist_generator,
|
||||
"params": x['class'].available_parameters(),
|
||||
"description": x['class'].description(),
|
||||
} for x in pairlists
|
||||
]}
|
||||
|
||||
|
||||
def __run_pairlist(job_id: str, config_loc: Config):
|
||||
try:
|
||||
|
||||
ApiBG.jobs[job_id]['is_running'] = True
|
||||
from freqtrade.plugins.pairlistmanager import PairListManager
|
||||
|
||||
exchange = get_exchange(config_loc)
|
||||
pairlists = PairListManager(exchange, config_loc)
|
||||
pairlists.refresh_pairlist()
|
||||
ApiBG.jobs[job_id]['result'] = {
|
||||
'method': pairlists.name_list,
|
||||
'length': len(pairlists.whitelist),
|
||||
'whitelist': pairlists.whitelist
|
||||
}
|
||||
ApiBG.jobs[job_id]['status'] = 'success'
|
||||
except (OperationalException, Exception) as e:
|
||||
logger.exception(e)
|
||||
ApiBG.jobs[job_id]['error'] = str(e)
|
||||
finally:
|
||||
ApiBG.jobs[job_id]['is_running'] = False
|
||||
ApiBG.jobs[job_id]['status'] = 'failed'
|
||||
ApiBG.pairlist_running = False
|
||||
|
||||
|
||||
@router.post('/pairlists/evaluate', response_model=BgJobStarted, tags=['pairlists', 'webserver'])
|
||||
def pairlists_evaluate(payload: PairListsPayload, background_tasks: BackgroundTasks,
|
||||
config=Depends(get_config)):
|
||||
if ApiBG.pairlist_running:
|
||||
raise HTTPException(status_code=400, detail='Pairlist evaluation is already running.')
|
||||
|
||||
config_loc = deepcopy(config)
|
||||
config_loc['stake_currency'] = payload.stake_currency
|
||||
config_loc['pairlists'] = payload.pairlists
|
||||
handleExchangePayload(payload, config_loc)
|
||||
# TODO: overwrite blacklist? make it optional and fall back to the one in config?
|
||||
# Outcome depends on the UI approach.
|
||||
config_loc['exchange']['pair_blacklist'] = payload.blacklist
|
||||
# Random job id
|
||||
job_id = ApiBG.get_job_id()
|
||||
|
||||
ApiBG.jobs[job_id] = {
|
||||
'category': 'pairlist',
|
||||
'status': 'pending',
|
||||
'progress': None,
|
||||
'is_running': False,
|
||||
'result': {},
|
||||
'error': None,
|
||||
}
|
||||
background_tasks.add_task(__run_pairlist, job_id, config_loc)
|
||||
ApiBG.pairlist_running = True
|
||||
|
||||
return {
|
||||
'status': 'Pairlist evaluation started in background.',
|
||||
'job_id': job_id,
|
||||
}
|
||||
|
||||
|
||||
def handleExchangePayload(payload: ExchangeModePayloadMixin, config_loc: Config):
|
||||
"""
|
||||
Handle exchange and trading mode payload.
|
||||
Updates the configuration with the payload values.
|
||||
"""
|
||||
if payload.exchange:
|
||||
config_loc['exchange']['name'] = payload.exchange
|
||||
if payload.trading_mode:
|
||||
config_loc['trading_mode'] = payload.trading_mode
|
||||
config_loc['candle_type_def'] = CandleType.get_default(
|
||||
config_loc.get('trading_mode', 'spot') or 'spot')
|
||||
if payload.margin_mode:
|
||||
config_loc['margin_mode'] = payload.margin_mode
|
||||
|
||||
|
||||
@router.get('/pairlists/evaluate/{jobid}', response_model=WhitelistEvaluateResponse,
|
||||
tags=['pairlists', 'webserver'])
|
||||
def pairlists_evaluate_get(jobid: str):
|
||||
if not (job := ApiBG.jobs.get(jobid)):
|
||||
raise HTTPException(status_code=404, detail='Job not found.')
|
||||
|
||||
if job['is_running']:
|
||||
raise HTTPException(status_code=400, detail='Job not finished yet.')
|
||||
|
||||
if error := job['error']:
|
||||
return {
|
||||
'status': 'failed',
|
||||
'error': error,
|
||||
}
|
||||
|
||||
return {
|
||||
'status': 'success',
|
||||
'result': job['result'],
|
||||
}
|
|
@ -4,10 +4,16 @@ from typing import Any, Dict, List, Optional, Union
|
|||
from pydantic import BaseModel
|
||||
|
||||
from freqtrade.constants import DATETIME_PRINT_FORMAT, IntOrInf
|
||||
from freqtrade.enums import OrderTypeValues, SignalDirection, TradingMode
|
||||
from freqtrade.enums import MarginMode, OrderTypeValues, SignalDirection, TradingMode
|
||||
from freqtrade.types import ValidExchangesType
|
||||
|
||||
|
||||
class ExchangeModePayloadMixin(BaseModel):
|
||||
trading_mode: Optional[TradingMode]
|
||||
margin_mode: Optional[MarginMode]
|
||||
exchange: Optional[str]
|
||||
|
||||
|
||||
class Ping(BaseModel):
|
||||
status: str
|
||||
|
||||
|
@ -28,6 +34,23 @@ class StatusMsg(BaseModel):
|
|||
status: str
|
||||
|
||||
|
||||
class BgJobStarted(StatusMsg):
|
||||
job_id: str
|
||||
|
||||
|
||||
class BackgroundTaskStatus(BaseModel):
|
||||
job_id: str
|
||||
job_category: str
|
||||
status: str
|
||||
running: bool
|
||||
progress: Optional[float]
|
||||
|
||||
|
||||
class BackgroundTaskResult(BaseModel):
|
||||
error: Optional[str]
|
||||
status: str
|
||||
|
||||
|
||||
class ResultMsg(BaseModel):
|
||||
result: str
|
||||
|
||||
|
@ -377,6 +400,10 @@ class WhitelistResponse(BaseModel):
|
|||
method: List[str]
|
||||
|
||||
|
||||
class WhitelistEvaluateResponse(BackgroundTaskResult):
|
||||
result: Optional[WhitelistResponse]
|
||||
|
||||
|
||||
class DeleteTrade(BaseModel):
|
||||
cancel_order_count: int
|
||||
result: str
|
||||
|
@ -401,6 +428,23 @@ class ExchangeListResponse(BaseModel):
|
|||
exchanges: List[ValidExchangesType]
|
||||
|
||||
|
||||
class PairListResponse(BaseModel):
|
||||
name: str
|
||||
description: str
|
||||
is_pairlist_generator: bool
|
||||
params: Dict[str, Any]
|
||||
|
||||
|
||||
class PairListsResponse(BaseModel):
|
||||
pairlists: List[PairListResponse]
|
||||
|
||||
|
||||
class PairListsPayload(ExchangeModePayloadMixin, BaseModel):
|
||||
pairlists: List[Dict[str, Any]]
|
||||
blacklist: List[str]
|
||||
stake_currency: str
|
||||
|
||||
|
||||
class FreqAIModelListResponse(BaseModel):
|
||||
freqaimodels: List[str]
|
||||
|
||||
|
|
|
@ -48,7 +48,8 @@ logger = logging.getLogger(__name__)
|
|||
# 2.27: Add /trades/<id>/reload endpoint
|
||||
# 2.28: Switch reload endpoint to Post
|
||||
# 2.29: Add /exchanges endpoint
|
||||
API_VERSION = 2.29
|
||||
# 2.30: new /pairlists endpoint
|
||||
API_VERSION = 2.30
|
||||
|
||||
# Public API, requires no auth.
|
||||
router_public = APIRouter()
|
||||
|
|
|
@ -3,6 +3,7 @@ from uuid import uuid4
|
|||
|
||||
from fastapi import Depends, HTTPException
|
||||
|
||||
from freqtrade.constants import Config
|
||||
from freqtrade.enums import RunMode
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.persistence.models import _request_id_ctx_var
|
||||
|
@ -43,12 +44,21 @@ def get_api_config() -> Dict[str, Any]:
|
|||
return ApiServer._config['api_server']
|
||||
|
||||
|
||||
def _generate_exchange_key(config: Config) -> str:
|
||||
"""
|
||||
Exchange key - used for caching the exchange object.
|
||||
"""
|
||||
return f"{config['exchange']['name']}_{config.get('trading_mode', 'spot')}"
|
||||
|
||||
|
||||
def get_exchange(config=Depends(get_config)):
|
||||
if not ApiBG.exchange:
|
||||
exchange_key = _generate_exchange_key(config)
|
||||
if not (exchange := ApiBG.exchanges.get(exchange_key)):
|
||||
from freqtrade.resolvers import ExchangeResolver
|
||||
ApiBG.exchange = ExchangeResolver.load_exchange(
|
||||
exchange = ExchangeResolver.load_exchange(
|
||||
config, load_leverage_tiers=False)
|
||||
return ApiBG.exchange
|
||||
ApiBG.exchanges[exchange_key] = exchange
|
||||
return exchange
|
||||
|
||||
|
||||
def get_message_stream():
|
||||
|
|
|
@ -114,6 +114,7 @@ class ApiServer(RPCHandler):
|
|||
|
||||
def configure_app(self, app: FastAPI, config):
|
||||
from freqtrade.rpc.api_server.api_auth import http_basic_or_jwt_token, router_login
|
||||
from freqtrade.rpc.api_server.api_background_tasks import router as api_bg_tasks
|
||||
from freqtrade.rpc.api_server.api_backtest import router as api_backtest
|
||||
from freqtrade.rpc.api_server.api_v1 import router as api_v1
|
||||
from freqtrade.rpc.api_server.api_v1 import router_public as api_v1_public
|
||||
|
@ -130,6 +131,10 @@ class ApiServer(RPCHandler):
|
|||
dependencies=[Depends(http_basic_or_jwt_token),
|
||||
Depends(is_webserver_mode)],
|
||||
)
|
||||
app.include_router(api_bg_tasks, prefix="/api/v1",
|
||||
dependencies=[Depends(http_basic_or_jwt_token),
|
||||
Depends(is_webserver_mode)],
|
||||
)
|
||||
app.include_router(ws_router, prefix="/api/v1")
|
||||
app.include_router(router_login, prefix="/api/v1", tags=["auth"])
|
||||
# UI Router MUST be last!
|
||||
|
|
|
@ -1,5 +1,17 @@
|
|||
|
||||
from typing import Any, Dict
|
||||
from typing import Any, Dict, Literal, Optional, TypedDict
|
||||
from uuid import uuid4
|
||||
|
||||
from freqtrade.exchange.exchange import Exchange
|
||||
|
||||
|
||||
class JobsContainer(TypedDict):
|
||||
category: Literal['pairlist']
|
||||
is_running: bool
|
||||
status: str
|
||||
progress: Optional[float]
|
||||
result: Any
|
||||
error: Optional[str]
|
||||
|
||||
|
||||
class ApiBG():
|
||||
|
@ -13,4 +25,15 @@ class ApiBG():
|
|||
}
|
||||
bgtask_running: bool = False
|
||||
# Exchange - only available in webserver mode.
|
||||
exchange = None
|
||||
exchanges: Dict[str, Exchange] = {}
|
||||
|
||||
# Generic background jobs
|
||||
|
||||
# TODO: Change this to TTLCache
|
||||
jobs: Dict[str, JobsContainer] = {}
|
||||
# Pairlist evaluate things
|
||||
pairlist_running: bool = False
|
||||
|
||||
@staticmethod
|
||||
def get_job_id() -> str:
|
||||
return str(uuid4())
|
||||
|
|
|
@ -534,10 +534,10 @@ class Telegram(RPCHandler):
|
|||
if order_nr == 1:
|
||||
lines.append(f"*{wording} #{order_nr}:*")
|
||||
lines.append(
|
||||
f"*Amount:* {cur_entry_amount} "
|
||||
f"*Amount:* {cur_entry_amount:.8g} "
|
||||
f"({round_coin_value(order['cost'], quote_currency)})"
|
||||
)
|
||||
lines.append(f"*Average Price:* {cur_entry_average}")
|
||||
lines.append(f"*Average Price:* {cur_entry_average:.8g}")
|
||||
else:
|
||||
sum_stake = 0
|
||||
sum_amount = 0
|
||||
|
@ -560,9 +560,9 @@ class Telegram(RPCHandler):
|
|||
if is_open:
|
||||
lines.append("({})".format(dt_humanize(order["order_filled_date"],
|
||||
granularity=["day", "hour", "minute"])))
|
||||
lines.append(f"*Amount:* {cur_entry_amount} "
|
||||
lines.append(f"*Amount:* {cur_entry_amount:.8g} "
|
||||
f"({round_coin_value(order['cost'], quote_currency)})")
|
||||
lines.append(f"*Average {wording} Price:* {cur_entry_average} "
|
||||
lines.append(f"*Average {wording} Price:* {cur_entry_average:.8g} "
|
||||
f"({price_to_1st_entry:.2%} from 1st entry Rate)")
|
||||
lines.append(f"*Order filled:* {order['order_filled_date']}")
|
||||
|
||||
|
@ -633,11 +633,11 @@ class Telegram(RPCHandler):
|
|||
])
|
||||
|
||||
lines.extend([
|
||||
"*Open Rate:* `{open_rate:.8f}`",
|
||||
"*Close Rate:* `{close_rate:.8f}`" if r['close_rate'] else "",
|
||||
"*Open Rate:* `{open_rate:.8g}`",
|
||||
"*Close Rate:* `{close_rate:.8g}`" if r['close_rate'] else "",
|
||||
"*Open Date:* `{open_date}`",
|
||||
"*Close Date:* `{close_date}`" if r['close_date'] else "",
|
||||
" \n*Current Rate:* `{current_rate:.8f}`" if r['is_open'] else "",
|
||||
" \n*Current Rate:* `{current_rate:.8g}`" if r['is_open'] else "",
|
||||
("*Unrealized Profit:* " if r['is_open'] else "*Close Profit: *")
|
||||
+ "`{profit_ratio:.2%}` `({profit_abs_r})`",
|
||||
])
|
||||
|
@ -658,9 +658,9 @@ class Telegram(RPCHandler):
|
|||
"`({initial_stop_loss_ratio:.2%})`")
|
||||
|
||||
# Adding stoploss and stoploss percentage only if it is not None
|
||||
lines.append("*Stoploss:* `{stop_loss_abs:.8f}` " +
|
||||
lines.append("*Stoploss:* `{stop_loss_abs:.8g}` " +
|
||||
("`({stop_loss_ratio:.2%})`" if r['stop_loss_ratio'] else ""))
|
||||
lines.append("*Stoploss distance:* `{stoploss_current_dist:.8f}` "
|
||||
lines.append("*Stoploss distance:* `{stoploss_current_dist:.8g}` "
|
||||
"`({stoploss_current_dist_ratio:.2%})`")
|
||||
if r['open_order']:
|
||||
lines.append(
|
||||
|
@ -1114,7 +1114,9 @@ class Telegram(RPCHandler):
|
|||
async def _force_exit_action(self, trade_id):
|
||||
if trade_id != 'cancel':
|
||||
try:
|
||||
self._rpc._rpc_force_exit(trade_id)
|
||||
loop = asyncio.get_running_loop()
|
||||
# Workaround to avoid nested loops
|
||||
await loop.run_in_executor(None, self._rpc._rpc_force_exit, trade_id)
|
||||
except RPCException as e:
|
||||
await self._send_msg(str(e))
|
||||
|
||||
|
@ -1140,7 +1142,11 @@ class Telegram(RPCHandler):
|
|||
async def _force_enter_action(self, pair, price: Optional[float], order_side: SignalDirection):
|
||||
if pair != 'cancel':
|
||||
try:
|
||||
self._rpc._rpc_force_entry(pair, price, order_side=order_side)
|
||||
def _force_enter():
|
||||
self._rpc._rpc_force_entry(pair, price, order_side=order_side)
|
||||
loop = asyncio.get_running_loop()
|
||||
# Workaround to avoid nested loops
|
||||
await loop.run_in_executor(None, _force_enter)
|
||||
except RPCException as e:
|
||||
logger.exception("Forcebuy error!")
|
||||
await self._send_msg(str(e), ParseMode.HTML)
|
||||
|
|
|
@ -1300,7 +1300,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||
timedout = (order.status == 'open' and order.order_date_utc < timeout_threshold)
|
||||
if timedout:
|
||||
return True
|
||||
time_method = (self.check_exit_timeout if order.side == trade.exit_side
|
||||
time_method = (self.check_exit_timeout if order.ft_order_side == trade.exit_side
|
||||
else self.check_entry_timeout)
|
||||
|
||||
return strategy_safe_wrapper(time_method,
|
||||
|
|
|
@ -232,7 +232,7 @@ class FreqaiExampleStrategy(IStrategy):
|
|||
|
||||
# All indicators must be populated by feature_engineering_*() functions
|
||||
|
||||
# the model will return all labels created by user in `feature_engineering_*`
|
||||
# the model will return all labels created by user in `set_freqai_targets()`
|
||||
# (& appended targets), an indication of whether or not the prediction should be accepted,
|
||||
# the target mean/std values for each of the labels created by user in
|
||||
# `set_freqai_targets()` for each training period.
|
||||
|
|
|
@ -7,10 +7,10 @@
|
|||
-r docs/requirements-docs.txt
|
||||
|
||||
coveralls==3.3.1
|
||||
ruff==0.0.270
|
||||
ruff==0.0.272
|
||||
mypy==1.3.0
|
||||
pre-commit==3.3.2
|
||||
pytest==7.3.1
|
||||
pytest==7.3.2
|
||||
pytest-asyncio==0.21.0
|
||||
pytest-cov==4.1.0
|
||||
pytest-mock==3.10.0
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
torch==2.0.1
|
||||
#until these branches will be released we can use this
|
||||
gymnasium==0.28.1
|
||||
stable_baselines3==2.0.0a10
|
||||
stable_baselines3==2.0.0a13
|
||||
sb3_contrib>=2.0.0a9
|
||||
# Progress bar for stable-baselines3 and sb3-contrib
|
||||
tqdm==4.65.0
|
||||
|
|
|
@ -5,8 +5,7 @@
|
|||
# Required for freqai
|
||||
scikit-learn==1.1.3
|
||||
joblib==1.2.0
|
||||
catboost==1.1.1; sys_platform == 'darwin' and python_version < '3.9'
|
||||
catboost==1.2; 'arm' not in platform_machine and (sys_platform != 'darwin' or python_version >= '3.9')
|
||||
catboost==1.2; 'arm' not in platform_machine
|
||||
lightgbm==3.3.5
|
||||
xgboost==1.7.5
|
||||
tensorboard==2.13.0
|
||||
|
|
|
@ -5,4 +5,4 @@
|
|||
scipy==1.10.1
|
||||
scikit-learn==1.1.3
|
||||
scikit-optimize==0.9.0
|
||||
filelock==3.12.0
|
||||
filelock==3.12.1
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Include all requirements to run the bot.
|
||||
-r requirements.txt
|
||||
|
||||
plotly==5.14.1
|
||||
plotly==5.15.0
|
||||
|
|
|
@ -2,18 +2,18 @@ numpy==1.24.3
|
|||
pandas==2.0.2
|
||||
pandas-ta==0.3.14b
|
||||
|
||||
ccxt==3.1.23
|
||||
ccxt==3.1.34
|
||||
cryptography==41.0.1; platform_machine != 'armv7l'
|
||||
cryptography==40.0.1; platform_machine == 'armv7l'
|
||||
aiohttp==3.8.4
|
||||
SQLAlchemy==2.0.15
|
||||
SQLAlchemy==2.0.16
|
||||
python-telegram-bot==20.3
|
||||
# can't be hard-pinned due to telegram-bot pinning httpx with ~
|
||||
httpx>=0.24.1
|
||||
arrow==1.2.3
|
||||
cachetools==5.3.1
|
||||
requests==2.31.0
|
||||
urllib3==2.0.2
|
||||
urllib3==2.0.3
|
||||
jsonschema==4.17.3
|
||||
TA-Lib==0.4.26
|
||||
technical==1.4.0
|
||||
|
@ -32,14 +32,14 @@ py_find_1st==1.1.5
|
|||
# Load ticker files 30% faster
|
||||
python-rapidjson==1.10
|
||||
# Properly format api responses
|
||||
orjson==3.9.0
|
||||
orjson==3.9.1
|
||||
|
||||
# Notify systemd
|
||||
sdnotify==0.3.2
|
||||
|
||||
# API Server
|
||||
fastapi==0.96.0
|
||||
pydantic==1.10.8
|
||||
fastapi==0.97.0
|
||||
pydantic==1.10.9
|
||||
uvicorn==0.22.0
|
||||
pyjwt==2.7.0
|
||||
aiofiles==23.1.0
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
2
setup.py
2
setup.py
|
@ -5,7 +5,7 @@ from setuptools import setup
|
|||
plot = ['plotly>=4.0']
|
||||
hyperopt = [
|
||||
'scipy',
|
||||
'scikit-learn',
|
||||
'scikit-learn<=1.1.3',
|
||||
'scikit-optimize>=0.7.0',
|
||||
'filelock',
|
||||
]
|
||||
|
|
|
@ -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()),
|
||||
|
|
|
@ -499,7 +499,11 @@ def test__set_leverage_okx(mocker, default_conf):
|
|||
assert api_mock.set_leverage.call_args_list[0][1]['params'] == {
|
||||
'mgnMode': 'isolated',
|
||||
'posSide': 'net'}
|
||||
api_mock.set_leverage = MagicMock(side_effect=ccxt.NetworkError())
|
||||
exchange._lev_prep('BTC/USDT:USDT', 3.2, 'buy')
|
||||
api_mock.fetch_leverage.call_count == 1
|
||||
|
||||
api_mock.fetch_leverage = MagicMock(side_effect=ccxt.NetworkError())
|
||||
ccxt_exceptionhandlers(
|
||||
mocker,
|
||||
default_conf,
|
||||
|
@ -592,3 +596,25 @@ def test_stoploss_adjust_okx(mocker, default_conf, sl1, sl2, sl3, side):
|
|||
}
|
||||
assert exchange.stoploss_adjust(sl1, order, side=side)
|
||||
assert not exchange.stoploss_adjust(sl2, order, side=side)
|
||||
|
||||
|
||||
def test_stoploss_cancel_okx(mocker, default_conf):
|
||||
exchange = get_patched_exchange(mocker, default_conf, id='okx')
|
||||
|
||||
exchange.cancel_order = MagicMock()
|
||||
|
||||
exchange.cancel_stoploss_order('1234', 'ETH/USDT')
|
||||
assert exchange.cancel_order.call_count == 1
|
||||
assert exchange.cancel_order.call_args_list[0][1]['order_id'] == '1234'
|
||||
assert exchange.cancel_order.call_args_list[0][1]['pair'] == 'ETH/USDT'
|
||||
assert exchange.cancel_order.call_args_list[0][1]['params'] == {'stop': True}
|
||||
|
||||
|
||||
def test__get_stop_params_okx(mocker, default_conf):
|
||||
default_conf['trading_mode'] = 'futures'
|
||||
default_conf['margin_mode'] = 'isolated'
|
||||
exchange = get_patched_exchange(mocker, default_conf, id='okx')
|
||||
params = exchange._get_stop_params('ETH/USDT:USDT', 1500, 'sell')
|
||||
|
||||
assert params['tdMode'] == 'isolated'
|
||||
assert params['posSide'] == 'net'
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
# pragma pylint: disable=missing-docstring, protected-access, invalid-name
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
import warnings
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
|
@ -23,8 +21,6 @@ from freqtrade.configuration.load_config import (load_config_file, load_file, lo
|
|||
from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL, ENV_VAR_PREFIX
|
||||
from freqtrade.enums import RunMode
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.loggers import (FTBufferingHandler, FTStdErrStreamHandler, _set_loggers,
|
||||
setup_logging, setup_logging_pre)
|
||||
from tests.conftest import (CURRENT_TEST_STRATEGY, log_has, log_has_re,
|
||||
patched_configuration_load_config_file)
|
||||
|
||||
|
@ -594,7 +590,7 @@ def test_cli_verbose_with_params(default_conf, mocker, caplog) -> None:
|
|||
patched_configuration_load_config_file(mocker, default_conf)
|
||||
|
||||
# Prevent setting loggers
|
||||
mocker.patch('freqtrade.loggers._set_loggers', MagicMock)
|
||||
mocker.patch('freqtrade.loggers.set_loggers', MagicMock)
|
||||
arglist = ['trade', '-vvv']
|
||||
args = Arguments(arglist).get_parsed_arg()
|
||||
|
||||
|
@ -605,127 +601,6 @@ def test_cli_verbose_with_params(default_conf, mocker, caplog) -> None:
|
|||
assert log_has('Verbosity set to 3', caplog)
|
||||
|
||||
|
||||
def test_set_loggers() -> None:
|
||||
# Reset Logging to Debug, otherwise this fails randomly as it's set globally
|
||||
logging.getLogger('requests').setLevel(logging.DEBUG)
|
||||
logging.getLogger("urllib3").setLevel(logging.DEBUG)
|
||||
logging.getLogger('ccxt.base.exchange').setLevel(logging.DEBUG)
|
||||
logging.getLogger('telegram').setLevel(logging.DEBUG)
|
||||
|
||||
previous_value1 = logging.getLogger('requests').level
|
||||
previous_value2 = logging.getLogger('ccxt.base.exchange').level
|
||||
previous_value3 = logging.getLogger('telegram').level
|
||||
|
||||
_set_loggers()
|
||||
|
||||
value1 = logging.getLogger('requests').level
|
||||
assert previous_value1 is not value1
|
||||
assert value1 is logging.INFO
|
||||
|
||||
value2 = logging.getLogger('ccxt.base.exchange').level
|
||||
assert previous_value2 is not value2
|
||||
assert value2 is logging.INFO
|
||||
|
||||
value3 = logging.getLogger('telegram').level
|
||||
assert previous_value3 is not value3
|
||||
assert value3 is logging.INFO
|
||||
|
||||
_set_loggers(verbosity=2)
|
||||
|
||||
assert logging.getLogger('requests').level is logging.DEBUG
|
||||
assert logging.getLogger('ccxt.base.exchange').level is logging.INFO
|
||||
assert logging.getLogger('telegram').level is logging.INFO
|
||||
assert logging.getLogger('werkzeug').level is logging.INFO
|
||||
|
||||
_set_loggers(verbosity=3, api_verbosity='error')
|
||||
|
||||
assert logging.getLogger('requests').level is logging.DEBUG
|
||||
assert logging.getLogger('ccxt.base.exchange').level is logging.DEBUG
|
||||
assert logging.getLogger('telegram').level is logging.INFO
|
||||
assert logging.getLogger('werkzeug').level is logging.ERROR
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.platform == "win32", reason="does not run on windows")
|
||||
def test_set_loggers_syslog():
|
||||
logger = logging.getLogger()
|
||||
orig_handlers = logger.handlers
|
||||
logger.handlers = []
|
||||
|
||||
config = {'verbosity': 2,
|
||||
'logfile': 'syslog:/dev/log',
|
||||
}
|
||||
|
||||
setup_logging_pre()
|
||||
setup_logging(config)
|
||||
assert len(logger.handlers) == 3
|
||||
assert [x for x in logger.handlers if type(x) == logging.handlers.SysLogHandler]
|
||||
assert [x for x in logger.handlers if type(x) == FTStdErrStreamHandler]
|
||||
assert [x for x in logger.handlers if type(x) == FTBufferingHandler]
|
||||
# setting up logging again should NOT cause the loggers to be added a second time.
|
||||
setup_logging(config)
|
||||
assert len(logger.handlers) == 3
|
||||
# reset handlers to not break pytest
|
||||
logger.handlers = orig_handlers
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.platform == "win32", reason="does not run on windows")
|
||||
def test_set_loggers_Filehandler(tmpdir):
|
||||
logger = logging.getLogger()
|
||||
orig_handlers = logger.handlers
|
||||
logger.handlers = []
|
||||
logfile = Path(tmpdir) / 'ft_logfile.log'
|
||||
config = {'verbosity': 2,
|
||||
'logfile': str(logfile),
|
||||
}
|
||||
|
||||
setup_logging_pre()
|
||||
setup_logging(config)
|
||||
assert len(logger.handlers) == 3
|
||||
assert [x for x in logger.handlers if type(x) == logging.handlers.RotatingFileHandler]
|
||||
assert [x for x in logger.handlers if type(x) == FTStdErrStreamHandler]
|
||||
assert [x for x in logger.handlers if type(x) == FTBufferingHandler]
|
||||
# setting up logging again should NOT cause the loggers to be added a second time.
|
||||
setup_logging(config)
|
||||
assert len(logger.handlers) == 3
|
||||
# reset handlers to not break pytest
|
||||
if logfile.exists:
|
||||
logfile.unlink()
|
||||
logger.handlers = orig_handlers
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="systemd is not installed on every system, so we're not testing this.")
|
||||
def test_set_loggers_journald(mocker):
|
||||
logger = logging.getLogger()
|
||||
orig_handlers = logger.handlers
|
||||
logger.handlers = []
|
||||
|
||||
config = {'verbosity': 2,
|
||||
'logfile': 'journald',
|
||||
}
|
||||
|
||||
setup_logging_pre()
|
||||
setup_logging(config)
|
||||
assert len(logger.handlers) == 3
|
||||
assert [x for x in logger.handlers if type(x).__name__ == "JournaldLogHandler"]
|
||||
assert [x for x in logger.handlers if type(x) == FTStdErrStreamHandler]
|
||||
# reset handlers to not break pytest
|
||||
logger.handlers = orig_handlers
|
||||
|
||||
|
||||
def test_set_loggers_journald_importerror(import_fails):
|
||||
logger = logging.getLogger()
|
||||
orig_handlers = logger.handlers
|
||||
logger.handlers = []
|
||||
|
||||
config = {'verbosity': 2,
|
||||
'logfile': 'journald',
|
||||
}
|
||||
with pytest.raises(OperationalException,
|
||||
match=r'You need the cysystemd python package.*'):
|
||||
setup_logging(config)
|
||||
logger.handlers = orig_handlers
|
||||
|
||||
|
||||
def test_set_logfile(default_conf, mocker, tmpdir):
|
||||
patched_configuration_load_config_file(mocker, default_conf)
|
||||
f = Path(tmpdir / "test_file.log")
|
||||
|
|
|
@ -1241,6 +1241,8 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_
|
|||
'type': 'stop_loss_limit',
|
||||
'price': 3,
|
||||
'average': 2,
|
||||
'filled': enter_order['amount'],
|
||||
'remaining': 0,
|
||||
'amount': enter_order['amount'],
|
||||
})
|
||||
mocker.patch(f'{EXMS}.fetch_stoploss_order', stoploss_order_hit)
|
||||
|
@ -3009,8 +3011,8 @@ def test_manage_open_orders_exit_usercustom(
|
|||
freqtrade.manage_open_orders()
|
||||
assert cancel_order_mock.call_count == 0
|
||||
assert rpc_mock.call_count == 1
|
||||
assert freqtrade.strategy.check_exit_timeout.call_count == 1
|
||||
assert freqtrade.strategy.check_entry_timeout.call_count == 0
|
||||
assert freqtrade.strategy.check_exit_timeout.call_count == (0 if is_short else 1)
|
||||
assert freqtrade.strategy.check_entry_timeout.call_count == (1 if is_short else 0)
|
||||
|
||||
freqtrade.strategy.check_exit_timeout = MagicMock(side_effect=KeyError)
|
||||
freqtrade.strategy.check_entry_timeout = MagicMock(side_effect=KeyError)
|
||||
|
@ -3018,8 +3020,8 @@ def test_manage_open_orders_exit_usercustom(
|
|||
freqtrade.manage_open_orders()
|
||||
assert cancel_order_mock.call_count == 0
|
||||
assert rpc_mock.call_count == 1
|
||||
assert freqtrade.strategy.check_exit_timeout.call_count == 1
|
||||
assert freqtrade.strategy.check_entry_timeout.call_count == 0
|
||||
assert freqtrade.strategy.check_exit_timeout.call_count == (0 if is_short else 1)
|
||||
assert freqtrade.strategy.check_entry_timeout.call_count == (1 if is_short else 0)
|
||||
|
||||
# Return True - sells!
|
||||
freqtrade.strategy.check_exit_timeout = MagicMock(return_value=True)
|
||||
|
@ -3027,8 +3029,8 @@ def test_manage_open_orders_exit_usercustom(
|
|||
freqtrade.manage_open_orders()
|
||||
assert cancel_order_mock.call_count == 1
|
||||
assert rpc_mock.call_count == 2
|
||||
assert freqtrade.strategy.check_exit_timeout.call_count == 1
|
||||
assert freqtrade.strategy.check_entry_timeout.call_count == 0
|
||||
assert freqtrade.strategy.check_exit_timeout.call_count == (0 if is_short else 1)
|
||||
assert freqtrade.strategy.check_entry_timeout.call_count == (1 if is_short else 0)
|
||||
trade = Trade.session.scalars(select(Trade)).first()
|
||||
# cancelling didn't succeed - order-id remains open.
|
||||
assert trade.open_order_id is not None
|
||||
|
|
130
tests/test_log_setup.py
Normal file
130
tests/test_log_setup.py
Normal file
|
@ -0,0 +1,130 @@
|
|||
import logging
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.loggers import (FTBufferingHandler, FTStdErrStreamHandler, set_loggers,
|
||||
setup_logging, setup_logging_pre)
|
||||
|
||||
|
||||
def test_set_loggers() -> None:
|
||||
# Reset Logging to Debug, otherwise this fails randomly as it's set globally
|
||||
logging.getLogger('requests').setLevel(logging.DEBUG)
|
||||
logging.getLogger("urllib3").setLevel(logging.DEBUG)
|
||||
logging.getLogger('ccxt.base.exchange').setLevel(logging.DEBUG)
|
||||
logging.getLogger('telegram').setLevel(logging.DEBUG)
|
||||
|
||||
previous_value1 = logging.getLogger('requests').level
|
||||
previous_value2 = logging.getLogger('ccxt.base.exchange').level
|
||||
previous_value3 = logging.getLogger('telegram').level
|
||||
|
||||
set_loggers()
|
||||
|
||||
value1 = logging.getLogger('requests').level
|
||||
assert previous_value1 is not value1
|
||||
assert value1 is logging.INFO
|
||||
|
||||
value2 = logging.getLogger('ccxt.base.exchange').level
|
||||
assert previous_value2 is not value2
|
||||
assert value2 is logging.INFO
|
||||
|
||||
value3 = logging.getLogger('telegram').level
|
||||
assert previous_value3 is not value3
|
||||
assert value3 is logging.INFO
|
||||
|
||||
set_loggers(verbosity=2)
|
||||
|
||||
assert logging.getLogger('requests').level is logging.DEBUG
|
||||
assert logging.getLogger('ccxt.base.exchange').level is logging.INFO
|
||||
assert logging.getLogger('telegram').level is logging.INFO
|
||||
assert logging.getLogger('werkzeug').level is logging.INFO
|
||||
|
||||
set_loggers(verbosity=3, api_verbosity='error')
|
||||
|
||||
assert logging.getLogger('requests').level is logging.DEBUG
|
||||
assert logging.getLogger('ccxt.base.exchange').level is logging.DEBUG
|
||||
assert logging.getLogger('telegram').level is logging.INFO
|
||||
assert logging.getLogger('werkzeug').level is logging.ERROR
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.platform == "win32", reason="does not run on windows")
|
||||
def test_set_loggers_syslog():
|
||||
logger = logging.getLogger()
|
||||
orig_handlers = logger.handlers
|
||||
logger.handlers = []
|
||||
|
||||
config = {'verbosity': 2,
|
||||
'logfile': 'syslog:/dev/log',
|
||||
}
|
||||
|
||||
setup_logging_pre()
|
||||
setup_logging(config)
|
||||
assert len(logger.handlers) == 3
|
||||
assert [x for x in logger.handlers if type(x) == logging.handlers.SysLogHandler]
|
||||
assert [x for x in logger.handlers if type(x) == FTStdErrStreamHandler]
|
||||
assert [x for x in logger.handlers if type(x) == FTBufferingHandler]
|
||||
# setting up logging again should NOT cause the loggers to be added a second time.
|
||||
setup_logging(config)
|
||||
assert len(logger.handlers) == 3
|
||||
# reset handlers to not break pytest
|
||||
logger.handlers = orig_handlers
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.platform == "win32", reason="does not run on windows")
|
||||
def test_set_loggers_Filehandler(tmpdir):
|
||||
logger = logging.getLogger()
|
||||
orig_handlers = logger.handlers
|
||||
logger.handlers = []
|
||||
logfile = Path(tmpdir) / 'ft_logfile.log'
|
||||
config = {'verbosity': 2,
|
||||
'logfile': str(logfile),
|
||||
}
|
||||
|
||||
setup_logging_pre()
|
||||
setup_logging(config)
|
||||
assert len(logger.handlers) == 3
|
||||
assert [x for x in logger.handlers if type(x) == logging.handlers.RotatingFileHandler]
|
||||
assert [x for x in logger.handlers if type(x) == FTStdErrStreamHandler]
|
||||
assert [x for x in logger.handlers if type(x) == FTBufferingHandler]
|
||||
# setting up logging again should NOT cause the loggers to be added a second time.
|
||||
setup_logging(config)
|
||||
assert len(logger.handlers) == 3
|
||||
# reset handlers to not break pytest
|
||||
if logfile.exists:
|
||||
logfile.unlink()
|
||||
logger.handlers = orig_handlers
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="systemd is not installed on every system, so we're not testing this.")
|
||||
def test_set_loggers_journald(mocker):
|
||||
logger = logging.getLogger()
|
||||
orig_handlers = logger.handlers
|
||||
logger.handlers = []
|
||||
|
||||
config = {'verbosity': 2,
|
||||
'logfile': 'journald',
|
||||
}
|
||||
|
||||
setup_logging_pre()
|
||||
setup_logging(config)
|
||||
assert len(logger.handlers) == 3
|
||||
assert [x for x in logger.handlers if type(x).__name__ == "JournaldLogHandler"]
|
||||
assert [x for x in logger.handlers if type(x) == FTStdErrStreamHandler]
|
||||
# reset handlers to not break pytest
|
||||
logger.handlers = orig_handlers
|
||||
|
||||
|
||||
def test_set_loggers_journald_importerror(import_fails):
|
||||
logger = logging.getLogger()
|
||||
orig_handlers = logger.handlers
|
||||
logger.handlers = []
|
||||
|
||||
config = {'verbosity': 2,
|
||||
'logfile': 'journald',
|
||||
}
|
||||
with pytest.raises(OperationalException,
|
||||
match=r'You need the cysystemd python package.*'):
|
||||
setup_logging(config)
|
||||
logger.handlers = orig_handlers
|
Loading…
Reference in New Issue
Block a user