Merge branch 'develop' into backtest_live_models

This commit is contained in:
Wagner Costa Santos
2022-10-13 11:22:58 -03:00
48 changed files with 373 additions and 197 deletions

View File

@@ -5,7 +5,7 @@ bot constants
"""
from typing import Any, Dict, List, Literal, Tuple
from freqtrade.enums import CandleType
from freqtrade.enums import CandleType, RPCMessageType
DEFAULT_CONFIG = 'config.json'
@@ -282,6 +282,7 @@ CONF_SCHEMA = {
'enabled': {'type': 'boolean'},
'token': {'type': 'string'},
'chat_id': {'type': 'string'},
'allow_custom_messages': {'type': 'boolean', 'default': True},
'balance_dust_level': {'type': 'number', 'minimum': 0.0},
'notification_settings': {
'type': 'object',
@@ -344,6 +345,8 @@ CONF_SCHEMA = {
'format': {'type': 'string', 'enum': WEBHOOK_FORMAT_OPTIONS, 'default': 'form'},
'retries': {'type': 'integer', 'minimum': 0},
'retry_delay': {'type': 'number', 'minimum': 0},
**dict([(x, {'type': 'object'}) for x in RPCMessageType]),
# Below -> Deprecated
'webhookentry': {'type': 'object'},
'webhookentrycancel': {'type': 'object'},
'webhookentryfill': {'type': 'object'},
@@ -653,5 +656,6 @@ LongShort = Literal['long', 'short']
EntryExit = Literal['entry', 'exit']
BuySell = Literal['buy', 'sell']
MakerTaker = Literal['maker', 'taker']
BidAsk = Literal['bid', 'ask']
Config = Dict[str, Any]

View File

@@ -11,6 +11,7 @@ from freqtrade.enums import CandleType, MarginMode, TradingMode
from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError
from freqtrade.exchange import Exchange
from freqtrade.exchange.common import retrier
from freqtrade.exchange.types import Tickers
from freqtrade.misc import deep_merge_dicts, json_load
@@ -59,7 +60,7 @@ class Binance(Exchange):
)
))
def get_tickers(self, symbols: Optional[List[str]] = None, cached: bool = False) -> Dict:
def get_tickers(self, symbols: Optional[List[str]] = None, cached: bool = False) -> Tickers:
tickers = super().get_tickers(symbols=symbols, cached=cached)
if self.trading_mode == TradingMode.FUTURES:
# Binance's future result has no bid/ask values.

View File

@@ -20,8 +20,8 @@ from ccxt import ROUND_DOWN, ROUND_UP, TICK_SIZE, TRUNCATE, decimal_to_precision
from dateutil import parser
from pandas import DataFrame, concat
from freqtrade.constants import (DEFAULT_AMOUNT_RESERVE_PERCENT, NON_OPEN_EXCHANGE_STATES, BuySell,
Config, EntryExit, ListPairsWithTimeframes, MakerTaker,
from freqtrade.constants import (DEFAULT_AMOUNT_RESERVE_PERCENT, NON_OPEN_EXCHANGE_STATES, BidAsk,
BuySell, Config, EntryExit, ListPairsWithTimeframes, MakerTaker,
PairWithTimeframe)
from freqtrade.data.converter import clean_ohlcv_dataframe, ohlcv_to_dataframe, trades_dict_to_list
from freqtrade.enums import OPTIMIZE_MODES, CandleType, MarginMode, TradingMode
@@ -31,6 +31,7 @@ from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFun
from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, BAD_EXCHANGES,
EXCHANGE_HAS_OPTIONAL, EXCHANGE_HAS_REQUIRED,
remove_credentials, retrier, retrier_async)
from freqtrade.exchange.types import Ticker, Tickers
from freqtrade.misc import (chunks, deep_merge_dicts, file_dump_json, file_load_json,
safe_value_fallback2)
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
@@ -1420,14 +1421,17 @@ class Exchange:
raise OperationalException(e) from e
@retrier
def get_tickers(self, symbols: Optional[List[str]] = None, cached: bool = False) -> Dict:
def get_tickers(self, symbols: Optional[List[str]] = None, cached: bool = False) -> Tickers:
"""
:param cached: Allow cached result
:return: fetch_tickers result
"""
tickers: Tickers
if not self.exchange_has('fetchTickers'):
return {}
if cached:
with self._cache_lock:
tickers = self._fetch_tickers_cache.get('fetch_tickers')
tickers = self._fetch_tickers_cache.get('fetch_tickers') # type: ignore
if tickers:
return tickers
try:
@@ -1450,12 +1454,12 @@ class Exchange:
# Pricing info
@retrier
def fetch_ticker(self, pair: str) -> dict:
def fetch_ticker(self, pair: str) -> Ticker:
try:
if (pair not in self.markets or
self.markets[pair].get('active', False) is False):
raise ExchangeError(f"Pair {pair} not available")
data = self._api.fetch_ticker(pair)
data: Ticker = self._api.fetch_ticker(pair)
return data
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
@@ -1506,7 +1510,7 @@ class Exchange:
except ccxt.BaseError as e:
raise OperationalException(e) from e
def _get_price_side(self, side: str, is_short: bool, conf_strategy: Dict) -> str:
def _get_price_side(self, side: str, is_short: bool, conf_strategy: Dict) -> BidAsk:
price_side = conf_strategy['price_side']
if price_side in ('same', 'other'):
@@ -1525,7 +1529,7 @@ class Exchange:
def get_rate(self, pair: str, refresh: bool,
side: EntryExit, is_short: bool,
order_book: Optional[dict] = None, ticker: Optional[dict] = None) -> float:
order_book: Optional[dict] = None, ticker: Optional[Ticker] = None) -> float:
"""
Calculates bid/ask target
bid rate - between current ask price and last price

View File

@@ -12,6 +12,7 @@ from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, Invali
OperationalException, TemporaryError)
from freqtrade.exchange import Exchange
from freqtrade.exchange.common import retrier
from freqtrade.exchange.types import Tickers
logger = logging.getLogger(__name__)
@@ -45,7 +46,7 @@ class Kraken(Exchange):
return (parent_check and
market.get('darkpool', False) is False)
def get_tickers(self, symbols: Optional[List[str]] = None, cached: bool = False) -> Dict:
def get_tickers(self, symbols: Optional[List[str]] = None, cached: bool = False) -> Tickers:
# Only fetch tickers for current stake currency
# Otherwise the request for kraken becomes too large.
symbols = list(self.get_markets(quote_currencies=[self._config['stake_currency']]))

View File

@@ -0,0 +1,16 @@
from typing import Dict, Optional, TypedDict
class Ticker(TypedDict):
symbol: str
ask: Optional[float]
askVolume: Optional[float]
bid: Optional[float]
bidVolume: Optional[float]
last: Optional[float]
quoteVolume: Optional[float]
baseVolume: Optional[float]
# Several more - only listing required.
Tickers = Dict[str, Ticker]

View File

@@ -245,6 +245,7 @@ class FreqaiDataKitchen:
self.data["filter_drop_index_training"] = drop_index
else:
filtered_df = self.check_pred_labels(filtered_df)
# we are backtesting so we need to preserve row number to send back to strategy,
# so now we use do_predict to avoid any prediction based on a NaN
drop_index = pd.isnull(filtered_df).any(axis=1)
@@ -487,6 +488,24 @@ class FreqaiDataKitchen:
return df
def check_pred_labels(self, df_predictions: DataFrame) -> DataFrame:
"""
Check that prediction feature labels match training feature labels.
:params:
:df_predictions: incoming predictions
"""
train_labels = self.data_dictionary["train_features"].columns
pred_labels = df_predictions.columns
num_diffs = len(pred_labels.difference(train_labels))
if num_diffs != 0:
df_predictions = df_predictions[train_labels]
logger.warning(
f"Removed {num_diffs} features from prediction features, "
f"these were likely considered constant values during most recent training."
)
return df_predictions
def principal_component_analysis(self) -> None:
"""
Performs Principal Component Analysis on the data for dimensionality reduction

View File

@@ -200,16 +200,15 @@ class IFreqaiModel(ABC):
(_, trained_timestamp, _) = self.dd.get_pair_dict_info(pair)
dk = FreqaiDataKitchen(self.config, self.live, pair)
dk.set_paths(pair, trained_timestamp)
(
retrain,
new_trained_timerange,
data_load_timerange,
) = dk.check_if_new_training_required(trained_timestamp)
dk.set_paths(pair, new_trained_timerange.stopts)
if retrain:
self.train_timer('start')
dk.set_paths(pair, new_trained_timerange.stopts)
try:
self.extract_data_and_train_model(
new_trained_timerange, pair, strategy, dk, data_load_timerange
@@ -290,9 +289,7 @@ class IFreqaiModel(ABC):
if dk.backtest_live_models:
timestamp_model_id = int(tr_backtest.startts)
dk.data_path = Path(
dk.full_path / f"sub-train-{pair.split('/')[0]}_{timestamp_model_id}"
)
dk.set_paths(pair, timestamp_model_id)
dk.set_new_model_names(pair, timestamp_model_id)

View File

@@ -1,4 +1,5 @@
import logging
from pathlib import Path
from typing import Any, Dict
from catboost import CatBoostClassifier, Pool
@@ -31,8 +32,9 @@ class CatboostClassifier(BaseClassifierModel):
)
cbr = CatBoostClassifier(
allow_writing_files=False,
allow_writing_files=True,
loss_function='MultiClass',
train_dir=Path(dk.data_path),
**self.model_training_parameters,
)

View File

@@ -1,4 +1,5 @@
import logging
from pathlib import Path
from typing import Any, Dict
from catboost import CatBoostRegressor, Pool
@@ -41,7 +42,8 @@ class CatboostRegressor(BaseRegressionModel):
init_model = self.get_init_model(dk.pair)
model = CatBoostRegressor(
allow_writing_files=False,
allow_writing_files=True,
train_dir=Path(dk.data_path),
**self.model_training_parameters,
)

View File

@@ -1,4 +1,5 @@
import logging
from pathlib import Path
from typing import Any, Dict
from catboost import CatBoostRegressor, Pool
@@ -26,7 +27,8 @@ class CatboostRegressorMultiTarget(BaseRegressionModel):
"""
cbr = CatBoostRegressor(
allow_writing_files=False,
allow_writing_files=True,
train_dir=Path(dk.data_path),
**self.model_training_parameters,
)

View File

@@ -6,7 +6,7 @@ import logging
import re
from datetime import datetime
from pathlib import Path
from typing import Any, Iterator, List
from typing import Any, Dict, Iterator, List, Mapping, Union
from typing.io import IO
from urllib.parse import urlparse
@@ -186,7 +186,10 @@ def safe_value_fallback(obj: dict, key1: str, key2: str, default_value=None):
return default_value
def safe_value_fallback2(dict1: dict, dict2: dict, key1: str, key2: str, default_value=None):
dictMap = Union[Dict[str, Any], Mapping[str, Any]]
def safe_value_fallback2(dict1: dictMap, dict2: dictMap, key1: str, key2: str, default_value=None):
"""
Search a value in dict1, return this if it's not None.
Fall back to dict2 - return key2 from dict2 if it's not None.

View File

@@ -10,6 +10,7 @@ from pandas import DataFrame
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.util import PeriodicCache
@@ -67,7 +68,7 @@ class AgeFilter(IPairList):
f"{self._max_days_listed} {plural(self._max_days_listed, 'day')}"
) if self._max_days_listed else '')
def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]:
def filter_pairlist(self, pairlist: List[str], tickers: Tickers) -> List[str]:
"""
:param pairlist: pairlist to filter or sort
:param tickers: Tickers (from exchange.get_tickers). May be cached.

View File

@@ -4,11 +4,12 @@ PairList Handler base class
import logging
from abc import ABC, abstractmethod, abstractproperty
from copy import deepcopy
from typing import Any, Dict, List
from typing import Any, Dict, List, Optional
from freqtrade.constants import Config
from freqtrade.exceptions import OperationalException
from freqtrade.exchange import Exchange, market_is_active
from freqtrade.exchange.types import Ticker, Tickers
from freqtrade.mixins import LoggingMixin
@@ -61,7 +62,7 @@ class IPairList(LoggingMixin, ABC):
-> Please overwrite in subclasses
"""
def _validate_pair(self, pair: str, ticker: Dict[str, Any]) -> bool:
def _validate_pair(self, pair: str, ticker: Optional[Ticker]) -> bool:
"""
Check one pair against Pairlist Handler's specific conditions.
@@ -74,7 +75,7 @@ class IPairList(LoggingMixin, ABC):
"""
raise NotImplementedError()
def gen_pairlist(self, tickers: Dict) -> List[str]:
def gen_pairlist(self, tickers: Tickers) -> List[str]:
"""
Generate the pairlist.
@@ -91,7 +92,7 @@ class IPairList(LoggingMixin, ABC):
raise OperationalException("This Pairlist Handler should not be used "
"at the first position in the list of Pairlist Handlers.")
def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]:
def filter_pairlist(self, pairlist: List[str], tickers: Tickers) -> List[str]:
"""
Filters and sorts pairlist and returns the whitelist again.
@@ -110,7 +111,7 @@ class IPairList(LoggingMixin, ABC):
# Copy list since we're modifying this list
for p in deepcopy(pairlist):
# Filter out assets
if not self._validate_pair(p, tickers[p] if p in tickers else {}):
if not self._validate_pair(p, tickers[p] if p in tickers else None):
pairlist.remove(p)
return pairlist

View File

@@ -6,6 +6,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
@@ -42,7 +43,7 @@ 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}."
def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]:
def filter_pairlist(self, pairlist: List[str], tickers: Tickers) -> List[str]:
"""
Filters and sorts pairlist and returns the whitelist again.
Called on each bot iteration - please use internal caching if necessary

View File

@@ -7,6 +7,7 @@ from typing import Any, Dict, List
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
@@ -39,7 +40,7 @@ class PerformanceFilter(IPairList):
"""
return f"{self.name} - Sorting pairs by performance."
def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]:
def filter_pairlist(self, pairlist: List[str], tickers: Tickers) -> List[str]:
"""
Filters and sorts pairlist and returns the allowlist again.
Called on each bot iteration - please use internal caching if necessary

View File

@@ -2,10 +2,11 @@
Precision pair list filter
"""
import logging
from typing import Any, Dict
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
@@ -44,7 +45,7 @@ class PrecisionFilter(IPairList):
"""
return f"{self.name} - Filtering untradable pairs."
def _validate_pair(self, pair: str, ticker: Dict[str, Any]) -> bool:
def _validate_pair(self, pair: str, ticker: Optional[Ticker]) -> bool:
"""
Check if pair has enough room to add a stoploss to avoid "unsellable" buys of very
low value pairs.
@@ -52,7 +53,7 @@ class PrecisionFilter(IPairList):
:param ticker: ticker dict as returned from ccxt.fetch_ticker
:return: True if the pair can stay, false if it should be removed
"""
if ticker.get('last', None) is None:
if not ticker or ticker.get('last', None) is None:
self.log_once(f"Removed {pair} from whitelist, because "
"ticker['last'] is empty (Usually no trade in the last 24h).",
logger.info)

View File

@@ -2,10 +2,11 @@
Price pair list filter
"""
import logging
from typing import Any, Dict
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
@@ -64,14 +65,16 @@ class PriceFilter(IPairList):
return f"{self.name} - No price filters configured."
def _validate_pair(self, pair: str, ticker: Dict[str, Any]) -> bool:
def _validate_pair(self, pair: str, ticker: Optional[Ticker]) -> bool:
"""
Check if if one price-step (pip) is > than a certain barrier.
:param pair: Pair that's currently validated
:param ticker: ticker dict as returned from ccxt.fetch_ticker
:return: True if the pair can stay, false if it should be removed
"""
if ticker.get('last', None) is None or ticker.get('last') == 0:
if ticker and 'last' in ticker and ticker['last'] is not None and ticker.get('last') != 0:
price: float = ticker['last']
else:
self.log_once(f"Removed {pair} from whitelist, because "
"ticker['last'] is empty (Usually no trade in the last 24h).",
logger.info)
@@ -79,8 +82,8 @@ class PriceFilter(IPairList):
# Perform low_price_ratio check.
if self._low_price_ratio != 0:
compare = self._exchange.price_get_one_pip(pair, ticker['last'])
changeperc = compare / ticker['last']
compare = self._exchange.price_get_one_pip(pair, price)
changeperc = compare / price
if changeperc > self._low_price_ratio:
self.log_once(f"Removed {pair} from whitelist, "
f"because 1 unit is {changeperc:.3%}", logger.info)
@@ -88,7 +91,6 @@ class PriceFilter(IPairList):
# Perform low_amount check
if self._max_value != 0:
price = ticker['last']
market = self._exchange.markets[pair]
limits = market['limits']
if (limits['amount']['min'] is not None):
@@ -113,14 +115,14 @@ class PriceFilter(IPairList):
# Perform min_price check.
if self._min_price != 0:
if ticker['last'] < self._min_price:
if price < self._min_price:
self.log_once(f"Removed {pair} from whitelist, "
f"because last price < {self._min_price:.8f}", logger.info)
return False
# Perform max_price check.
if self._max_price != 0:
if ticker['last'] > self._max_price:
if price > self._max_price:
self.log_once(f"Removed {pair} from whitelist, "
f"because last price > {self._max_price:.8f}", logger.info)
return False

View File

@@ -7,6 +7,7 @@ import logging
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
@@ -68,7 +69,7 @@ class ProducerPairList(IPairList):
return pairs
def gen_pairlist(self, tickers: Dict) -> List[str]:
def gen_pairlist(self, tickers: Tickers) -> List[str]:
"""
Generate the pairlist
:param tickers: Tickers (from exchange.get_tickers). May be cached.
@@ -79,7 +80,7 @@ class ProducerPairList(IPairList):
pairs = self._whitelist_for_active_markets(self.verify_whitelist(pairs, logger.info))
return pairs
def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]:
def filter_pairlist(self, pairlist: List[str], tickers: Tickers) -> List[str]:
"""
Filters and sorts pairlist and returns the whitelist again.
Called on each bot iteration - please use internal caching if necessary

View File

@@ -7,6 +7,7 @@ from typing import Any, Dict, List
from freqtrade.constants import Config
from freqtrade.enums import RunMode
from freqtrade.exchange.types import Tickers
from freqtrade.plugins.pairlist.IPairList import IPairList
@@ -47,7 +48,7 @@ class ShuffleFilter(IPairList):
return (f"{self.name} - Shuffling pairs" +
(f", seed = {self._seed}." if self._seed is not None else "."))
def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]:
def filter_pairlist(self, pairlist: List[str], tickers: Tickers) -> List[str]:
"""
Filters and sorts pairlist and returns the whitelist again.
Called on each bot iteration - please use internal caching if necessary

View File

@@ -2,10 +2,10 @@
Spread pair list filter
"""
import logging
from typing import Any, Dict
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
@@ -22,12 +22,6 @@ class SpreadFilter(IPairList):
self._max_spread_ratio = pairlistconfig.get('max_spread_ratio', 0.005)
self._enabled = self._max_spread_ratio != 0
if not self._exchange.exchange_has('fetchTickers'):
raise OperationalException(
'Exchange does not support fetchTickers, therefore SpreadFilter cannot be used.'
'Please edit your config and restart the bot.'
)
@property
def needstickers(self) -> bool:
"""
@@ -44,14 +38,14 @@ class SpreadFilter(IPairList):
return (f"{self.name} - Filtering pairs with ask/bid diff above "
f"{self._max_spread_ratio:.2%}.")
def _validate_pair(self, pair: str, ticker: Dict[str, Any]) -> bool:
def _validate_pair(self, pair: str, ticker: Optional[Ticker]) -> bool:
"""
Validate spread for the ticker
:param pair: Pair that's currently validated
:param ticker: ticker dict as returned from ccxt.fetch_ticker
:return: True if the pair can stay, false if it should be removed
"""
if 'bid' in ticker and 'ask' in ticker and ticker['ask'] and ticker['bid']:
if ticker and 'bid' in ticker and 'ask' in ticker and ticker['ask'] and ticker['bid']:
spread = 1 - ticker['bid'] / ticker['ask']
if spread > self._max_spread_ratio:
self.log_once(f"Removed {pair} from whitelist, because spread "

View File

@@ -8,6 +8,7 @@ from copy import deepcopy
from typing import Any, Dict, List
from freqtrade.constants import Config
from freqtrade.exchange.types import Tickers
from freqtrade.plugins.pairlist.IPairList import IPairList
@@ -39,7 +40,7 @@ class StaticPairList(IPairList):
"""
return f"{self.name}"
def gen_pairlist(self, tickers: Dict) -> List[str]:
def gen_pairlist(self, tickers: Tickers) -> List[str]:
"""
Generate the pairlist
:param tickers: Tickers (from exchange.get_tickers). May be cached.
@@ -53,7 +54,7 @@ class StaticPairList(IPairList):
return self._whitelist_for_active_markets(
self.verify_whitelist(self._config['exchange']['pair_whitelist'], logger.info))
def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]:
def filter_pairlist(self, pairlist: List[str], tickers: Tickers) -> List[str]:
"""
Filters and sorts pairlist and returns the whitelist again.
Called on each bot iteration - please use internal caching if necessary

View File

@@ -13,6 +13,7 @@ from pandas import DataFrame
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
@@ -62,7 +63,7 @@ class VolatilityFilter(IPairList):
f"{self._min_volatility}-{self._max_volatility} "
f" the last {self._days} {plural(self._days, 'day')}.")
def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]:
def filter_pairlist(self, pairlist: List[str], tickers: Tickers) -> List[str]:
"""
Validate trading range
:param pairlist: pairlist to filter or sort

View File

@@ -5,13 +5,14 @@ Provides dynamic pair list based on trade volumes
"""
import logging
from datetime import datetime, timedelta, timezone
from typing import Any, Dict, List
from typing import Any, Dict, List, Literal
from cachetools import TTLCache
from freqtrade.constants import Config, ListPairsWithTimeframes
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
@@ -36,7 +37,7 @@ class VolumePairList(IPairList):
self._stake_currency = config['stake_currency']
self._number_pairs = self._pairlistconfig['number_assets']
self._sort_key = self._pairlistconfig.get('sort_key', 'quoteVolume')
self._sort_key: Literal['quoteVolume'] = self._pairlistconfig.get('sort_key', 'quoteVolume')
self._min_value = self._pairlistconfig.get('min_value', 0)
self._refresh_period = self._pairlistconfig.get('refresh_period', 1800)
self._pair_cache: TTLCache = TTLCache(maxsize=1, ttl=self._refresh_period)
@@ -110,7 +111,7 @@ class VolumePairList(IPairList):
"""
return f"{self.name} - top {self._pairlistconfig['number_assets']} volume pairs."
def gen_pairlist(self, tickers: Dict) -> List[str]:
def gen_pairlist(self, tickers: Tickers) -> List[str]:
"""
Generate the pairlist
:param tickers: Tickers (from exchange.get_tickers). May be cached.

View File

@@ -11,6 +11,7 @@ from pandas import DataFrame
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
@@ -60,7 +61,7 @@ class RangeStabilityFilter(IPairList):
f"{self._min_rate_of_change}{max_rate_desc} over the "
f"last {plural(self._days, 'day')}.")
def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]:
def filter_pairlist(self, pairlist: List[str], tickers: Tickers) -> List[str]:
"""
Validate trading range
:param pairlist: pairlist to filter or sort

View File

@@ -11,6 +11,7 @@ from freqtrade.constants import Config, ListPairsWithTimeframes
from freqtrade.data.dataprovider import DataProvider
from freqtrade.enums import CandleType
from freqtrade.exceptions import OperationalException
from freqtrade.exchange.types import Tickers
from freqtrade.mixins import LoggingMixin
from freqtrade.plugins.pairlist.IPairList import IPairList
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
@@ -45,6 +46,15 @@ class PairListManager(LoggingMixin):
if not self._pairlist_handlers:
raise OperationalException("No Pairlist Handlers defined")
if self._tickers_needed and not self._exchange.exchange_has('fetchTickers'):
invalid = ". ".join([p.name for p in self._pairlist_handlers if p.needstickers])
raise OperationalException(
"Exchange does not support fetchTickers, therefore the following pairlists "
"cannot be used. Please edit your config and restart the bot.\n"
f"{invalid}."
)
refresh_period = config.get('pairlist_refresh_period', 3600)
LoggingMixin.__init__(self, logger, refresh_period)
@@ -76,7 +86,7 @@ class PairListManager(LoggingMixin):
return [{p.name: p.short_desc()} for p in self._pairlist_handlers]
@cached(TTLCache(maxsize=1, ttl=1800))
def _get_cached_tickers(self):
def _get_cached_tickers(self) -> Tickers:
return self._exchange.get_tickers()
def refresh_pairlist(self) -> None:

View File

@@ -4,6 +4,7 @@ from typing import Any, Dict
from fastapi import APIRouter, Depends, WebSocketDisconnect
from fastapi.websockets import WebSocket, WebSocketState
from pydantic import ValidationError
from websockets.exceptions import WebSocketException
from freqtrade.enums import RPCMessageType, RPCRequestType
from freqtrade.rpc.api_server.api_auth import validate_ws_token
@@ -102,7 +103,6 @@ async def message_endpoint(
"""
try:
channel = await channel_manager.on_connect(ws)
if await is_websocket_alive(ws):
logger.info(f"Consumer connected - {channel}")
@@ -115,26 +115,31 @@ async def message_endpoint(
# Process the request here
await _process_consumer_request(request, channel, rpc)
except WebSocketDisconnect:
except (WebSocketDisconnect, WebSocketException):
# Handle client disconnects
logger.info(f"Consumer disconnected - {channel}")
await channel_manager.on_disconnect(ws)
except Exception as e:
logger.info(f"Consumer connection failed - {channel}")
logger.exception(e)
except RuntimeError:
# Handle cases like -
# RuntimeError('Cannot call "send" once a closed message has been sent')
pass
except Exception as e:
logger.info(f"Consumer connection failed - {channel}: {e}")
logger.debug(e, exc_info=e)
finally:
await channel_manager.on_disconnect(ws)
else:
if channel:
await channel_manager.on_disconnect(ws)
await ws.close()
except RuntimeError:
# WebSocket was closed
await channel_manager.on_disconnect(ws)
# Do nothing
pass
except Exception as e:
logger.error(f"Failed to serve - {ws.client}")
# Log tracebacks to keep track of what errors are happening
logger.exception(e)
finally:
await channel_manager.on_disconnect(ws)

View File

@@ -198,10 +198,6 @@ class ApiServer(RPCHandler):
logger.debug(f"Found message of type: {message.get('type')}")
# Broadcast it
await self._ws_channel_manager.broadcast(message)
# Limit messages per sec.
# Could cause problems with queue size if too low, and
# problems with network traffik if too high.
await asyncio.sleep(0.001)
except asyncio.CancelledError:
pass
@@ -245,6 +241,7 @@ class ApiServer(RPCHandler):
use_colors=False,
log_config=None,
access_log=True if verbosity != 'error' else False,
ws_ping_interval=None # We do this explicitly ourselves
)
try:
self._server = UvicornServer(uvconfig)

View File

@@ -1,6 +1,7 @@
import asyncio
import logging
from threading import RLock
from typing import List, Optional, Type
from typing import Any, Dict, List, Optional, Type
from uuid import uuid4
from fastapi import WebSocket as FastAPIWebSocket
@@ -34,6 +35,8 @@ class WebSocketChannel:
self._serializer_cls = serializer_cls
self._subscriptions: List[str] = []
self.queue: asyncio.Queue[Dict[str, Any]] = asyncio.Queue(maxsize=32)
self._relay_task = asyncio.create_task(self.relay())
# Internal event to signify a closed websocket
self._closed = False
@@ -48,12 +51,18 @@ class WebSocketChannel:
def remote_addr(self):
return self._websocket.remote_addr
async def send(self, data):
async def _send(self, data):
"""
Send data on the wrapped websocket
"""
await self._wrapped_ws.send(data)
async def send(self, data):
"""
Add the data to the queue to be sent
"""
self.queue.put_nowait(data)
async def recv(self):
"""
Receive data on the wrapped websocket
@@ -72,6 +81,7 @@ class WebSocketChannel:
"""
self._closed = True
self._relay_task.cancel()
def is_closed(self) -> bool:
"""
@@ -95,6 +105,26 @@ class WebSocketChannel:
"""
return message_type in self._subscriptions
async def relay(self):
"""
Relay messages from the channel's queue and send them out. This is started
as a task.
"""
while True:
message = await self.queue.get()
try:
await self._send(message)
self.queue.task_done()
# Limit messages per sec.
# Could cause problems with queue size if too low, and
# problems with network traffik if too high.
# 0.001 = 1000/s
await asyncio.sleep(0.001)
except RuntimeError:
# The connection was closed, just exit the task
return
class ChannelManager:
def __init__(self):
@@ -155,12 +185,12 @@ class ChannelManager:
with self._lock:
message_type = data.get('type')
for websocket, channel in self.channels.copy().items():
try:
if channel.subscribed_to(message_type):
if channel.subscribed_to(message_type):
if not channel.queue.full():
await channel.send(data)
except RuntimeError:
# Handle cannot send after close cases
await self.on_disconnect(websocket)
else:
logger.info(f"Channel {channel} is too far behind, disconnecting")
await self.on_disconnect(websocket)
async def send_direct(self, channel, data):
"""

View File

@@ -11,13 +11,12 @@ logger = logging.getLogger(__name__)
class Discord(Webhook):
def __init__(self, rpc: 'RPC', config: Config):
# super().__init__(rpc, config)
self._config = config
self.rpc = rpc
self.config = config
self.strategy = config.get('strategy', '')
self.timeframe = config.get('timeframe', '')
self._url = self.config['discord']['webhook_url']
self._url = config['discord']['webhook_url']
self._format = 'json'
self._retries = 1
self._retry_delay = 0.1
@@ -31,19 +30,21 @@ class Discord(Webhook):
def send_msg(self, msg) -> None:
if msg['type'].value in self.config['discord']:
if msg['type'].value in self._config['discord']:
logger.info(f"Sending discord message: {msg}")
msg['strategy'] = self.strategy
msg['timeframe'] = self.timeframe
fields = self.config['discord'].get(msg['type'].value)
fields = self._config['discord'].get(msg['type'].value)
color = 0x0000FF
if msg['type'] in (RPCMessageType.EXIT, RPCMessageType.EXIT_FILL):
profit_ratio = msg.get('profit_ratio')
color = (0x00FF00 if profit_ratio > 0 else 0xFF0000)
title = msg['type'].value
if 'pair' in msg:
title = f"Trade: {msg['pair']} {msg['type'].value}"
embeds = [{
'title': f"Trade: {msg['pair']} {msg['type'].value}",
'title': title,
'color': color,
'fields': [],
@@ -51,7 +52,7 @@ class Discord(Webhook):
for f in fields:
for k, v in f.items():
v = v.format(**msg)
embeds[0]['fields'].append( # type: ignore
embeds[0]['fields'].append(
{'name': k, 'value': v, 'inline': True})
# Send the message to discord channel

View File

@@ -62,7 +62,7 @@ class ExternalMessageConsumer:
self.enabled = self._emc_config.get('enabled', False)
self.producers: List[Producer] = self._emc_config.get('producers', [])
self.wait_timeout = self._emc_config.get('wait_timeout', 300) # in seconds
self.wait_timeout = self._emc_config.get('wait_timeout', 30) # in seconds
self.ping_timeout = self._emc_config.get('ping_timeout', 10) # in seconds
self.sleep_time = self._emc_config.get('sleep_time', 10) # in seconds
@@ -174,6 +174,7 @@ class ExternalMessageConsumer:
:param producer: Dictionary containing producer info
:param lock: An asyncio Lock
"""
channel = None
while self._running:
try:
host, port = producer['host'], producer['port']
@@ -182,7 +183,11 @@ class ExternalMessageConsumer:
ws_url = f"ws://{host}:{port}/api/v1/message/ws?token={token}"
# This will raise InvalidURI if the url is bad
async with websockets.connect(ws_url, max_size=self.message_size_limit) as ws:
async with websockets.connect(
ws_url,
max_size=self.message_size_limit,
ping_interval=None
) as ws:
channel = WebSocketChannel(ws, channel_id=name)
logger.info(f"Producer connection success - {channel}")
@@ -224,6 +229,10 @@ class ExternalMessageConsumer:
logger.exception(e)
continue
finally:
if channel:
await channel.close()
async def _receive_messages(
self,
channel: WebSocketChannel,

View File

@@ -88,10 +88,13 @@ class RPCManager:
"""
while queue:
msg = queue.popleft()
self.send_msg({
'type': RPCMessageType.STRATEGY_MSG,
'msg': msg,
})
logger.info('Sending rpc strategy_msg: %s', msg)
for mod in self.registered_modules:
if mod._config.get(mod.name, {}).get('allow_custom_messages', False):
mod.send_msg({
'type': RPCMessageType.STRATEGY_MSG,
'msg': msg,
})
def startup_messages(self, config: Config, pairlist, protections) -> None:
if config['dry_run']:

View File

@@ -3,7 +3,7 @@ This module manages webhook communication
"""
import logging
import time
from typing import Any, Dict
from typing import Any, Dict, Optional
from requests import RequestException, post
@@ -41,36 +41,44 @@ class Webhook(RPCHandler):
"""
pass
def _get_value_dict(self, msg: Dict[str, Any]) -> Optional[Dict[str, Any]]:
whconfig = self._config['webhook']
# Deprecated 2022.10 - only keep generic method.
if msg['type'] in [RPCMessageType.ENTRY]:
valuedict = whconfig.get('webhookentry')
elif msg['type'] in [RPCMessageType.ENTRY_CANCEL]:
valuedict = whconfig.get('webhookentrycancel')
elif msg['type'] in [RPCMessageType.ENTRY_FILL]:
valuedict = whconfig.get('webhookentryfill')
elif msg['type'] == RPCMessageType.EXIT:
valuedict = whconfig.get('webhookexit')
elif msg['type'] == RPCMessageType.EXIT_FILL:
valuedict = whconfig.get('webhookexitfill')
elif msg['type'] == RPCMessageType.EXIT_CANCEL:
valuedict = whconfig.get('webhookexitcancel')
elif msg['type'] in (RPCMessageType.STATUS,
RPCMessageType.STARTUP,
RPCMessageType.WARNING):
valuedict = whconfig.get('webhookstatus')
elif msg['type'].value in whconfig:
# Allow all types ...
valuedict = whconfig.get(msg['type'].value)
elif msg['type'] in (
RPCMessageType.PROTECTION_TRIGGER,
RPCMessageType.PROTECTION_TRIGGER_GLOBAL,
RPCMessageType.WHITELIST,
RPCMessageType.ANALYZED_DF,
RPCMessageType.STRATEGY_MSG):
# Don't fail for non-implemented types
return None
return valuedict
def send_msg(self, msg: Dict[str, Any]) -> None:
""" Send a message to telegram channel """
try:
whconfig = self._config['webhook']
if msg['type'] in [RPCMessageType.ENTRY]:
valuedict = whconfig.get('webhookentry')
elif msg['type'] in [RPCMessageType.ENTRY_CANCEL]:
valuedict = whconfig.get('webhookentrycancel')
elif msg['type'] in [RPCMessageType.ENTRY_FILL]:
valuedict = whconfig.get('webhookentryfill')
elif msg['type'] == RPCMessageType.EXIT:
valuedict = whconfig.get('webhookexit')
elif msg['type'] == RPCMessageType.EXIT_FILL:
valuedict = whconfig.get('webhookexitfill')
elif msg['type'] == RPCMessageType.EXIT_CANCEL:
valuedict = whconfig.get('webhookexitcancel')
elif msg['type'] in (RPCMessageType.STATUS,
RPCMessageType.STARTUP,
RPCMessageType.WARNING):
valuedict = whconfig.get('webhookstatus')
elif msg['type'] in (
RPCMessageType.PROTECTION_TRIGGER,
RPCMessageType.PROTECTION_TRIGGER_GLOBAL,
RPCMessageType.WHITELIST,
RPCMessageType.ANALYZED_DF,
RPCMessageType.STRATEGY_MSG):
# Don't fail for non-implemented types
return
else:
raise NotImplementedError('Unknown message type: {}'.format(msg['type']))
valuedict = self._get_value_dict(msg)
if not valuedict:
logger.info("Message type '%s' not configured for webhooks", msg['type'])
return

View File

@@ -49,7 +49,7 @@ class IStrategy(ABC, HyperStrategyMixin):
_ft_params_from_file: Dict
# associated minimal roi
minimal_roi: Dict = {}
minimal_roi: Dict = {"0": 10.0}
# associated stoploss
stoploss: float