Merge branch 'develop' into pr/th0rntwig/7495
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
""" Freqtrade bot """
|
||||
__version__ = '2022.9.dev'
|
||||
__version__ = '2022.10.dev'
|
||||
|
||||
if 'dev' in __version__:
|
||||
try:
|
||||
|
@@ -31,7 +31,7 @@ HYPEROPT_LOSS_BUILTIN = ['ShortTradeDurHyperOptLoss', 'OnlyProfitHyperOptLoss',
|
||||
'CalmarHyperOptLoss',
|
||||
'MaxDrawDownHyperOptLoss', 'MaxDrawDownRelativeHyperOptLoss',
|
||||
'ProfitDrawDownHyperOptLoss']
|
||||
AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList',
|
||||
AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'ProducerPairList',
|
||||
'AgeFilter', 'OffsetFilter', 'PerformanceFilter',
|
||||
'PrecisionFilter', 'PriceFilter', 'RangeStabilityFilter',
|
||||
'ShuffleFilter', 'SpreadFilter', 'VolatilityFilter']
|
||||
|
@@ -47,8 +47,7 @@ def ohlcv_to_dataframe(ohlcv: list, timeframe: str, pair: str, *,
|
||||
|
||||
|
||||
def clean_ohlcv_dataframe(data: DataFrame, timeframe: str, pair: str, *,
|
||||
fill_missing: bool = True,
|
||||
drop_incomplete: bool = True) -> DataFrame:
|
||||
fill_missing: bool, drop_incomplete: bool) -> DataFrame:
|
||||
"""
|
||||
Cleanse a OHLCV dataframe by
|
||||
* Grouping it by date (removes duplicate tics)
|
||||
|
@@ -26,7 +26,7 @@ def load_pair_history(pair: str,
|
||||
datadir: Path, *,
|
||||
timerange: Optional[TimeRange] = None,
|
||||
fill_up_missing: bool = True,
|
||||
drop_incomplete: bool = True,
|
||||
drop_incomplete: bool = False,
|
||||
startup_candles: int = 0,
|
||||
data_format: str = None,
|
||||
data_handler: IDataHandler = None,
|
||||
|
@@ -275,7 +275,7 @@ class IDataHandler(ABC):
|
||||
candle_type: CandleType, *,
|
||||
timerange: Optional[TimeRange] = None,
|
||||
fill_missing: bool = True,
|
||||
drop_incomplete: bool = True,
|
||||
drop_incomplete: bool = False,
|
||||
startup_candles: int = 0,
|
||||
warn_no_data: bool = True,
|
||||
) -> DataFrame:
|
||||
|
@@ -68,6 +68,37 @@ class Binance(Exchange):
|
||||
tickers = deep_merge_dicts(bidsasks, tickers, allow_null_overrides=False)
|
||||
return tickers
|
||||
|
||||
@retrier
|
||||
def additional_exchange_init(self) -> None:
|
||||
"""
|
||||
Additional exchange initialization logic.
|
||||
.api will be available at this point.
|
||||
Must be overridden in child methods if required.
|
||||
"""
|
||||
try:
|
||||
if self.trading_mode == TradingMode.FUTURES and not self._config['dry_run']:
|
||||
position_side = self._api.fapiPrivateGetPositionsideDual()
|
||||
self._log_exchange_response('position_side_setting', position_side)
|
||||
assets_margin = self._api.fapiPrivateGetMultiAssetsMargin()
|
||||
self._log_exchange_response('multi_asset_margin', assets_margin)
|
||||
msg = ""
|
||||
if position_side.get('dualSidePosition') is True:
|
||||
msg += (
|
||||
"\nHedge Mode is not supported by freqtrade. "
|
||||
"Please change 'Position Mode' on your binance futures account.")
|
||||
if assets_margin.get('multiAssetsMargin') is True:
|
||||
msg += ("\nMulti-Asset Mode is not supported by freqtrade. "
|
||||
"Please change 'Asset Mode' on your binance futures account.")
|
||||
if msg:
|
||||
raise OperationalException(msg)
|
||||
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
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
@retrier
|
||||
def _set_leverage(
|
||||
self,
|
||||
|
@@ -78,7 +78,8 @@ class Okx(Exchange):
|
||||
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
|
||||
f'Error in additional_exchange_init due to {e.__class__.__name__}. Message: {e}'
|
||||
) from e
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
|
@@ -204,7 +204,7 @@ class FreqaiDataKitchen:
|
||||
filtered_df = unfiltered_df.filter(training_feature_list, axis=1)
|
||||
filtered_df = filtered_df.replace([np.inf, -np.inf], np.nan)
|
||||
|
||||
drop_index = pd.isnull(filtered_df).any(1) # get the rows that have NaNs,
|
||||
drop_index = pd.isnull(filtered_df).any(axis=1) # get the rows that have NaNs,
|
||||
drop_index = drop_index.replace(True, 1).replace(False, 0) # pep8 requirement.
|
||||
if (training_filter):
|
||||
const_cols = list((filtered_df.nunique() == 1).loc[lambda x: x].index)
|
||||
@@ -215,7 +215,7 @@ class FreqaiDataKitchen:
|
||||
# about removing any row with NaNs
|
||||
# if labels has multiple columns (user wants to train multiple modelEs), we detect here
|
||||
labels = unfiltered_df.filter(label_list, axis=1)
|
||||
drop_index_labels = pd.isnull(labels).any(1)
|
||||
drop_index_labels = pd.isnull(labels).any(axis=1)
|
||||
drop_index_labels = drop_index_labels.replace(True, 1).replace(False, 0)
|
||||
dates = unfiltered_df['date']
|
||||
filtered_df = filtered_df[
|
||||
@@ -243,7 +243,7 @@ class FreqaiDataKitchen:
|
||||
else:
|
||||
# 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(1)
|
||||
drop_index = pd.isnull(filtered_df).any(axis=1)
|
||||
self.data["filter_drop_index_prediction"] = drop_index
|
||||
filtered_df.fillna(0, inplace=True)
|
||||
# replacing all NaNs with zeros to avoid issues in 'prediction', but any prediction
|
||||
@@ -802,7 +802,7 @@ class FreqaiDataKitchen:
|
||||
:, :no_prev_pts
|
||||
]
|
||||
distances = distances.replace([np.inf, -np.inf], np.nan)
|
||||
drop_index = pd.isnull(distances).any(1)
|
||||
drop_index = pd.isnull(distances).any(axis=1)
|
||||
distances = distances[drop_index == 0]
|
||||
|
||||
inliers = pd.DataFrame(index=distances.index)
|
||||
|
@@ -82,7 +82,10 @@ class FreqtradeBot(LoggingMixin):
|
||||
# Keep this at the end of this initialization method.
|
||||
self.rpc: RPCManager = RPCManager(self)
|
||||
|
||||
self.dataprovider = DataProvider(self.config, self.exchange, self.pairlists, self.rpc)
|
||||
self.dataprovider = DataProvider(self.config, self.exchange, rpc=self.rpc)
|
||||
self.pairlists = PairListManager(self.exchange, self.config, self.dataprovider)
|
||||
|
||||
self.dataprovider.add_pairlisthandler(self.pairlists)
|
||||
|
||||
# Attach Dataprovider to strategy instance
|
||||
self.strategy.dp = self.dataprovider
|
||||
@@ -597,7 +600,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
# We should decrease our position
|
||||
amount = self.exchange.amount_to_contract_precision(
|
||||
trade.pair,
|
||||
abs(float(FtPrecise(stake_amount) / FtPrecise(current_exit_rate))))
|
||||
abs(float(FtPrecise(stake_amount * trade.leverage) / FtPrecise(current_exit_rate))))
|
||||
if amount > trade.amount:
|
||||
# This is currently ineffective as remaining would become < min tradable
|
||||
# Fixing this would require checking for 0.0 there -
|
||||
@@ -1340,11 +1343,12 @@ class FreqtradeBot(LoggingMixin):
|
||||
replacing: Optional[bool] = False
|
||||
) -> bool:
|
||||
"""
|
||||
Buy cancel - cancel order
|
||||
entry cancel - cancel order
|
||||
:param replacing: Replacing order - prevent trade deletion.
|
||||
:return: True if order was fully cancelled
|
||||
:return: True if trade was fully cancelled
|
||||
"""
|
||||
was_trade_fully_canceled = False
|
||||
side = trade.entry_side.capitalize()
|
||||
|
||||
# Cancelled orders may have the status of 'canceled' or 'closed'
|
||||
if order['status'] not in constants.NON_OPEN_EXCHANGE_STATES:
|
||||
@@ -1371,7 +1375,6 @@ class FreqtradeBot(LoggingMixin):
|
||||
corder = order
|
||||
reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE']
|
||||
|
||||
side = trade.entry_side.capitalize()
|
||||
logger.info('%s order %s for %s.', side, reason, trade)
|
||||
|
||||
# Using filled to determine the filled amount
|
||||
@@ -1385,24 +1388,13 @@ class FreqtradeBot(LoggingMixin):
|
||||
was_trade_fully_canceled = True
|
||||
reason += f", {constants.CANCEL_REASON['FULLY_CANCELLED']}"
|
||||
else:
|
||||
# FIXME TODO: This could possibly reworked to not duplicate the code 15 lines below.
|
||||
self.update_trade_state(trade, trade.open_order_id, corder)
|
||||
trade.open_order_id = None
|
||||
logger.info(f'{side} Order timeout for {trade}.')
|
||||
else:
|
||||
# if trade is partially complete, edit the stake details for the trade
|
||||
# and close the order
|
||||
# cancel_order may not contain the full order dict, so we need to fallback
|
||||
# to the order dict acquired before cancelling.
|
||||
# we need to fall back to the values from order if corder does not contain these keys.
|
||||
trade.amount = filled_amount
|
||||
# * Check edge cases, we don't want to make leverage > 1.0 if we don't have to
|
||||
# * (for leverage modes which aren't isolated futures)
|
||||
|
||||
trade.stake_amount = trade.amount * trade.open_rate / trade.leverage
|
||||
# update_trade_state (and subsequently recalc_trade_from_orders) will handle updates
|
||||
# to the trade object
|
||||
self.update_trade_state(trade, trade.open_order_id, corder)
|
||||
|
||||
trade.open_order_id = None
|
||||
logger.info(f'Partial {trade.entry_side} order timeout for {trade}.')
|
||||
reason += f", {constants.CANCEL_REASON['PARTIALLY_FILLED']}"
|
||||
|
||||
@@ -1439,8 +1431,6 @@ class FreqtradeBot(LoggingMixin):
|
||||
trade.close_rate_requested = None
|
||||
trade.close_profit = None
|
||||
trade.close_profit_abs = None
|
||||
trade.close_date = None
|
||||
trade.is_open = True
|
||||
trade.open_order_id = None
|
||||
trade.exit_reason = None
|
||||
cancelled = True
|
||||
@@ -1700,11 +1690,6 @@ class FreqtradeBot(LoggingMixin):
|
||||
'stake_amount': trade.stake_amount,
|
||||
}
|
||||
|
||||
if 'fiat_display_currency' in self.config:
|
||||
msg.update({
|
||||
'fiat_currency': self.config['fiat_display_currency'],
|
||||
})
|
||||
|
||||
# Send the message
|
||||
self.rpc.send_msg(msg)
|
||||
|
||||
|
@@ -110,7 +110,7 @@ class Backtesting:
|
||||
self.timeframe = str(self.config.get('timeframe'))
|
||||
self.timeframe_min = timeframe_to_minutes(self.timeframe)
|
||||
self.init_backtest_detail()
|
||||
self.pairlists = PairListManager(self.exchange, self.config)
|
||||
self.pairlists = PairListManager(self.exchange, self.config, self.dataprovider)
|
||||
if 'VolumePairList' in self.pairlists.name_list:
|
||||
raise OperationalException("VolumePairList not allowed for backtesting. "
|
||||
"Please use StaticPairList instead.")
|
||||
@@ -540,7 +540,7 @@ class Backtesting:
|
||||
|
||||
if stake_amount is not None and stake_amount < 0.0:
|
||||
amount = amount_to_contract_precision(
|
||||
abs(stake_amount) / current_rate, trade.amount_precision,
|
||||
abs(stake_amount * trade.leverage) / current_rate, trade.amount_precision,
|
||||
self.precision_mode, trade.contract_size)
|
||||
if amount == 0.0:
|
||||
return trade
|
||||
|
90
freqtrade/plugins/pairlist/ProducerPairList.py
Normal file
90
freqtrade/plugins/pairlist/ProducerPairList.py
Normal file
@@ -0,0 +1,90 @@
|
||||
"""
|
||||
External Pair List provider
|
||||
|
||||
Provides pair list from Leader data
|
||||
"""
|
||||
import logging
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ProducerPairList(IPairList):
|
||||
"""
|
||||
PairList plugin for use with external_message_consumer.
|
||||
Will use pairs given from leader data.
|
||||
|
||||
Usage:
|
||||
"pairlists": [
|
||||
{
|
||||
"method": "ProducerPairList",
|
||||
"number_assets": 5,
|
||||
"producer_name": "default",
|
||||
}
|
||||
],
|
||||
"""
|
||||
|
||||
def __init__(self, exchange, pairlistmanager,
|
||||
config: Dict[str, Any], pairlistconfig: Dict[str, Any],
|
||||
pairlist_pos: int) -> None:
|
||||
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
||||
|
||||
self._num_assets: int = self._pairlistconfig.get('number_assets', 0)
|
||||
self._producer_name = self._pairlistconfig.get('producer_name', 'default')
|
||||
if not config.get('external_message_consumer', {}).get('enabled'):
|
||||
raise OperationalException(
|
||||
"ProducerPairList requires external_message_consumer to be enabled.")
|
||||
|
||||
@property
|
||||
def needstickers(self) -> bool:
|
||||
"""
|
||||
Boolean property defining if tickers are necessary.
|
||||
If no Pairlist requires tickers, an empty Dict is passed
|
||||
as tickers argument to filter_pairlist
|
||||
"""
|
||||
return False
|
||||
|
||||
def short_desc(self) -> str:
|
||||
"""
|
||||
Short whitelist method description - used for startup-messages
|
||||
-> Please overwrite in subclasses
|
||||
"""
|
||||
return f"{self.name} - {self._producer_name}"
|
||||
|
||||
def _filter_pairlist(self, pairlist: Optional[List[str]]):
|
||||
upstream_pairlist = self._pairlistmanager._dataprovider.get_producer_pairs(
|
||||
self._producer_name)
|
||||
|
||||
if pairlist is None:
|
||||
pairlist = self._pairlistmanager._dataprovider.get_producer_pairs(self._producer_name)
|
||||
|
||||
pairs = list(dict.fromkeys(pairlist + upstream_pairlist))
|
||||
if self._num_assets:
|
||||
pairs = pairs[:self._num_assets]
|
||||
|
||||
return pairs
|
||||
|
||||
def gen_pairlist(self, tickers: Dict) -> List[str]:
|
||||
"""
|
||||
Generate the pairlist
|
||||
:param tickers: Tickers (from exchange.get_tickers()). May be cached.
|
||||
:return: List of pairs
|
||||
"""
|
||||
pairs = self._filter_pairlist(None)
|
||||
self.log_once(f"Received pairs: {pairs}", logger.debug)
|
||||
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]:
|
||||
"""
|
||||
Filters and sorts pairlist and returns the whitelist again.
|
||||
Called on each bot iteration - please use internal caching if necessary
|
||||
:param pairlist: pairlist to filter or sort
|
||||
:param tickers: Tickers (from exchange.get_tickers()). May be cached.
|
||||
:return: new whitelist
|
||||
"""
|
||||
return self._filter_pairlist(pairlist)
|
@@ -232,6 +232,4 @@ class VolumePairList(IPairList):
|
||||
# Limit pairlist to the requested number of pairs
|
||||
pairs = pairs[:self._number_pairs]
|
||||
|
||||
self.log_once(f"Searching {self._number_pairs} pairs: {pairs}", logger.info)
|
||||
|
||||
return pairs
|
||||
|
@@ -3,11 +3,12 @@ PairList manager class
|
||||
"""
|
||||
import logging
|
||||
from functools import partial
|
||||
from typing import Dict, List
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from cachetools import TTLCache, cached
|
||||
|
||||
from freqtrade.constants import Config, ListPairsWithTimeframes
|
||||
from freqtrade.data.dataprovider import DataProvider
|
||||
from freqtrade.enums import CandleType
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.mixins import LoggingMixin
|
||||
@@ -21,13 +22,14 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
class PairListManager(LoggingMixin):
|
||||
|
||||
def __init__(self, exchange, config: Config) -> None:
|
||||
def __init__(self, exchange, config: Config, dataprovider: DataProvider = None) -> None:
|
||||
self._exchange = exchange
|
||||
self._config = config
|
||||
self._whitelist = self._config['exchange'].get('pair_whitelist')
|
||||
self._blacklist = self._config['exchange'].get('pair_blacklist', [])
|
||||
self._pairlist_handlers: List[IPairList] = []
|
||||
self._tickers_needed = False
|
||||
self._dataprovider: Optional[DataProvider] = dataprovider
|
||||
for pairlist_handler_config in self._config.get('pairlists', []):
|
||||
pairlist_handler = PairListResolver.load_pairlist(
|
||||
pairlist_handler_config['method'],
|
||||
@@ -96,6 +98,8 @@ class PairListManager(LoggingMixin):
|
||||
# to ensure blacklist is respected.
|
||||
pairlist = self.verify_blacklist(pairlist, logger.warning)
|
||||
|
||||
self.log_once(f"Whitelist with {len(pairlist)} pairs: {pairlist}", logger.info)
|
||||
|
||||
self._whitelist = pairlist
|
||||
|
||||
def verify_blacklist(self, pairlist: List[str], logmethod) -> List[str]:
|
||||
|
@@ -30,9 +30,9 @@ class Discord(Webhook):
|
||||
pass
|
||||
|
||||
def send_msg(self, msg) -> None:
|
||||
logger.info(f"Sending discord message: {msg}")
|
||||
|
||||
if msg['type'].value in self.config['discord']:
|
||||
logger.info(f"Sending discord message: {msg}")
|
||||
|
||||
msg['strategy'] = self.strategy
|
||||
msg['timeframe'] = self.timeframe
|
||||
|
@@ -284,7 +284,7 @@ class ExternalMessageConsumer:
|
||||
logger.error(f"Empty message received from `{producer_name}`")
|
||||
return
|
||||
|
||||
logger.info(f"Received message of type `{producer_message.type}` from `{producer_name}`")
|
||||
logger.debug(f"Received message of type `{producer_message.type}` from `{producer_name}`")
|
||||
|
||||
message_handler = self._message_handlers.get(producer_message.type)
|
||||
|
||||
|
@@ -67,7 +67,7 @@ class RPCManager:
|
||||
'status': 'stopping bot'
|
||||
}
|
||||
"""
|
||||
if msg.get('type') is not RPCMessageType.ANALYZED_DF:
|
||||
if msg.get('type') not in (RPCMessageType.ANALYZED_DF, RPCMessageType.WHITELIST):
|
||||
logger.info('Sending rpc message: %s', msg)
|
||||
if 'pair' in msg:
|
||||
msg.update({
|
||||
|
@@ -61,6 +61,14 @@ class Webhook(RPCHandler):
|
||||
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']))
|
||||
if not valuedict:
|
||||
|
Reference in New Issue
Block a user