diff --git a/freqtrade/commands/build_config_commands.py b/freqtrade/commands/build_config_commands.py index 58ac6ec27..87098f53c 100644 --- a/freqtrade/commands/build_config_commands.py +++ b/freqtrade/commands/build_config_commands.py @@ -163,7 +163,7 @@ def deploy_new_config(config_path: Path, selections: Dict[str, Any]) -> None: ) except TemplateNotFound: selections['exchange'] = render_template( - templatefile=f"subtemplates/exchange_generic.j2", + templatefile="subtemplates/exchange_generic.j2", arguments=selections ) diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index a8f2ffdba..ee9208c33 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -372,8 +372,8 @@ AVAILABLE_CLI_OPTIONS = { ), "timeframes": Arg( '-t', '--timeframes', - help=f'Specify which tickers to download. Space-separated list. ' - f'Default: `1m 5m`.', + help='Specify which tickers to download. Space-separated list. ' + 'Default: `1m 5m`.', choices=['1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h', '6h', '8h', '12h', '1d', '3d', '1w'], default=['1m', '5m'], diff --git a/freqtrade/commands/deploy_commands.py b/freqtrade/commands/deploy_commands.py index a29ba346f..86562fa7c 100644 --- a/freqtrade/commands/deploy_commands.py +++ b/freqtrade/commands/deploy_commands.py @@ -51,7 +51,7 @@ def deploy_new_strategy(strategy_name: str, strategy_path: Path, subtemplate: st ) additional_methods = render_template_with_fallback( templatefile=f"subtemplates/strategy_methods_{subtemplate}.j2", - templatefallbackfile=f"subtemplates/strategy_methods_empty.j2", + templatefallbackfile="subtemplates/strategy_methods_empty.j2", ) strategy_text = render_template(templatefile='base_strategy.py.j2', diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 858cdecab..36d1ddca4 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -367,8 +367,7 @@ class Exchange: f"Invalid timeframe '{timeframe}'. This exchange supports: {self.timeframes}") if timeframe and timeframe_to_minutes(timeframe) < 1: - raise OperationalException( - f"Timeframes < 1m are currently not supported by Freqtrade.") + raise OperationalException("Timeframes < 1m are currently not supported by Freqtrade.") def validate_ordertypes(self, order_types: Dict) -> None: """ diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 6797788ee..d5d512e78 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -874,10 +874,10 @@ class FreqtradeBot: logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc()) continue - trade_state_update = self.update_trade_state(trade, order) + fully_cancelled = self.update_trade_state(trade, order) - if (order['side'] == 'buy' and ( - trade_state_update + if (order['side'] == 'buy' and (order['status'] == 'open' or fully_cancelled) and ( + fully_cancelled or self._check_timed_out('buy', order) or strategy_safe_wrapper(self.strategy.check_buy_timeout, default_retval=False)(pair=trade.pair, @@ -885,8 +885,8 @@ class FreqtradeBot: order=order))): self.handle_cancel_buy(trade, order, constants.CANCEL_REASON['TIMEOUT']) - elif (order['side'] == 'sell' and ( - trade_state_update + elif (order['side'] == 'sell' and (order['status'] == 'open' or fully_cancelled) and ( + fully_cancelled or self._check_timed_out('sell', order) or strategy_safe_wrapper(self.strategy.check_sell_timeout, default_retval=False)(pair=trade.pair, @@ -1121,6 +1121,11 @@ class FreqtradeBot: """ Sends rpc notification when a sell cancel occured. """ + if trade.sell_order_status == reason: + return + else: + trade.sell_order_status = reason + profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested profit_trade = trade.calc_profit(rate=profit_rate) current_rate = self.get_sell_rate(trade.pair, False) diff --git a/freqtrade/pairlist/IPairList.py b/freqtrade/pairlist/IPairList.py index e089e546c..e2eb364bc 100644 --- a/freqtrade/pairlist/IPairList.py +++ b/freqtrade/pairlist/IPairList.py @@ -1,9 +1,6 @@ """ -Static List provider - -Provides lists as configured in config.json - - """ +PairList base class +""" import logging from abc import ABC, abstractmethod, abstractproperty from copy import deepcopy @@ -13,6 +10,7 @@ from cachetools import TTLCache, cached from freqtrade.exchange import market_is_active + logger = logging.getLogger(__name__) diff --git a/freqtrade/pairlist/PrecisionFilter.py b/freqtrade/pairlist/PrecisionFilter.py index 2a2ba46b7..6bd9c594e 100644 --- a/freqtrade/pairlist/PrecisionFilter.py +++ b/freqtrade/pairlist/PrecisionFilter.py @@ -1,14 +1,26 @@ +""" +Precision pair list filter +""" import logging from copy import deepcopy -from typing import Dict, List +from typing import Any, Dict, List from freqtrade.pairlist.IPairList import IPairList + logger = logging.getLogger(__name__) class PrecisionFilter(IPairList): + 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) + + # Precalculate sanitized stoploss value to avoid recalculation for every pair + self._stoploss = 1 - abs(self._config['stoploss']) + @property def needstickers(self) -> bool: """ @@ -31,34 +43,32 @@ class PrecisionFilter(IPairList): :param ticker: ticker dict as returned from ccxt.load_markets() :param stoploss: stoploss value as set in the configuration (already cleaned to be 1 - stoploss) - :return: True if the pair can stay, false if it should be removed + :return: True if the pair can stay, False if it should be removed """ stop_price = ticker['ask'] * stoploss + # Adjust stop-prices to precision sp = self._exchange.price_to_precision(ticker["symbol"], stop_price) + stop_gap_price = self._exchange.price_to_precision(ticker["symbol"], stop_price * 0.99) logger.debug(f"{ticker['symbol']} - {sp} : {stop_gap_price}") + if sp <= stop_gap_price: self.log_on_refresh(logger.info, f"Removed {ticker['symbol']} from whitelist, " f"because stop price {sp} would be <= stop limit {stop_gap_price}") return False + return True def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]: """ Filters and sorts pairlists and assigns and returns them again. """ - stoploss = self._config.get('stoploss') - if stoploss is not None: - # Precalculate sanitized stoploss value to avoid recalculation for every pair - stoploss = 1 - abs(stoploss) # Copy list since we're modifying this list for p in deepcopy(pairlist): - ticker = tickers.get(p) # Filter out assets which would not allow setting a stoploss - if not ticker or (stoploss and not self._validate_precision_filter(ticker, stoploss)): + if not self._validate_precision_filter(tickers[p], self._stoploss): pairlist.remove(p) - continue return pairlist diff --git a/freqtrade/pairlist/PriceFilter.py b/freqtrade/pairlist/PriceFilter.py index 2f7e98e24..167717656 100644 --- a/freqtrade/pairlist/PriceFilter.py +++ b/freqtrade/pairlist/PriceFilter.py @@ -1,9 +1,13 @@ +""" +Price pair list filter +""" import logging from copy import deepcopy from typing import Any, Dict, List from freqtrade.pairlist.IPairList import IPairList + logger = logging.getLogger(__name__) @@ -38,14 +42,12 @@ class PriceFilter(IPairList): :return: True if the pair can stay, false if it should be removed """ if ticker['last'] is None: - self.log_on_refresh(logger.info, f"Removed {ticker['symbol']} from whitelist, because " "ticker['last'] is empty (Usually no trade in the last 24h).") return False - compare = ticker['last'] + self._exchange.price_get_one_pip(ticker['symbol'], - ticker['last']) - changeperc = (compare - ticker['last']) / ticker['last'] + compare = self._exchange.price_get_one_pip(ticker['symbol'], ticker['last']) + changeperc = compare / ticker['last'] if changeperc > self._low_price_ratio: self.log_on_refresh(logger.info, f"Removed {ticker['symbol']} from whitelist, " f"because 1 unit is {changeperc * 100:.3f}%") @@ -60,14 +62,11 @@ class PriceFilter(IPairList): :param tickers: Tickers (from exchange.get_tickers()). May be cached. :return: new whitelist """ - # Copy list since we're modifying this list - for p in deepcopy(pairlist): - ticker = tickers.get(p) - if not ticker: - pairlist.remove(p) - - # Filter out assets which would not allow setting a stoploss - if self._low_price_ratio and not self._validate_ticker_lowprice(ticker): - pairlist.remove(p) + if self._low_price_ratio: + # Copy list since we're modifying this list + for p in deepcopy(pairlist): + # Filter out assets which would not allow setting a stoploss + if not self._validate_ticker_lowprice(tickers[p]): + pairlist.remove(p) return pairlist diff --git a/freqtrade/pairlist/SpreadFilter.py b/freqtrade/pairlist/SpreadFilter.py index 49731ef11..88e143a50 100644 --- a/freqtrade/pairlist/SpreadFilter.py +++ b/freqtrade/pairlist/SpreadFilter.py @@ -1,9 +1,13 @@ +""" +Spread pair list filter +""" import logging from copy import deepcopy from typing import Dict, List from freqtrade.pairlist.IPairList import IPairList + logger = logging.getLogger(__name__) @@ -31,8 +35,24 @@ class SpreadFilter(IPairList): return (f"{self.name} - Filtering pairs with ask/bid diff above " f"{self._max_spread_ratio * 100}%.") - def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]: + def _validate_spread(self, ticker: dict) -> bool: + """ + Validate spread for the ticker + :param ticker: ticker dict as returned from ccxt.load_markets() + :return: True if the pair can stay, False if it should be removed + """ + if 'bid' in ticker and 'ask' in ticker: + spread = 1 - ticker['bid'] / ticker['ask'] + if spread > self._max_spread_ratio: + self.log_on_refresh(logger.info, f"Removed {ticker['symbol']} from whitelist, " + f"because spread {spread * 100:.3f}% >" + f"{self._max_spread_ratio * 100}%") + return False + else: + return True + return False + 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 @@ -41,19 +61,10 @@ class SpreadFilter(IPairList): :return: new whitelist """ # Copy list since we're modifying this list - - spread = None for p in deepcopy(pairlist): - ticker = tickers.get(p) - assert ticker is not None - if 'bid' in ticker and 'ask' in ticker: - spread = 1 - ticker['bid'] / ticker['ask'] - if not ticker or spread > self._max_spread_ratio: - self.log_on_refresh(logger.info, f"Removed {ticker['symbol']} from whitelist, " - f"because spread {spread * 100:.3f}% >" - f"{self._max_spread_ratio * 100}%") - pairlist.remove(p) - else: + ticker = tickers[p] + # Filter out assets + if not self._validate_spread(ticker): pairlist.remove(p) return pairlist diff --git a/freqtrade/pairlist/StaticPairList.py b/freqtrade/pairlist/StaticPairList.py index 0050fbd5c..07e559168 100644 --- a/freqtrade/pairlist/StaticPairList.py +++ b/freqtrade/pairlist/StaticPairList.py @@ -1,14 +1,14 @@ """ -Static List provider +Static Pair List provider -Provides lists as configured in config.json - - """ +Provides pair white list as it configured in config +""" import logging from typing import Dict, List from freqtrade.pairlist.IPairList import IPairList + logger = logging.getLogger(__name__) diff --git a/freqtrade/pairlist/VolumePairList.py b/freqtrade/pairlist/VolumePairList.py index eb44fe725..981e9915e 100644 --- a/freqtrade/pairlist/VolumePairList.py +++ b/freqtrade/pairlist/VolumePairList.py @@ -1,9 +1,8 @@ """ Volume PairList provider -Provides lists as configured in config.json - - """ +Provides dynamic pair list based on trade volumes +""" import logging from datetime import datetime from typing import Any, Dict, List @@ -11,8 +10,10 @@ from typing import Any, Dict, List from freqtrade.exceptions import OperationalException from freqtrade.pairlist.IPairList import IPairList + logger = logging.getLogger(__name__) + SORT_VALUES = ['askVolume', 'bidVolume', 'quoteVolume'] @@ -24,8 +25,10 @@ class VolumePairList(IPairList): if 'number_assets' not in self._pairlistconfig: raise OperationalException( - f'`number_assets` not specified. Please check your configuration ' + '`number_assets` not specified. Please check your configuration ' 'for "pairlist.config.number_assets"') + + self._stake_currency = config['stake_currency'] self._number_pairs = self._pairlistconfig['number_assets'] self._sort_key = self._pairlistconfig.get('sort_key', 'quoteVolume') self._min_value = self._pairlistconfig.get('min_value', 0) @@ -36,9 +39,11 @@ class VolumePairList(IPairList): 'Exchange does not support dynamic whitelist.' 'Please edit your config and restart the bot' ) + if not self._validate_keys(self._sort_key): raise OperationalException( f'key {self._sort_key} not in {SORT_VALUES}') + if self._sort_key != 'quoteVolume': logger.warning( "DEPRECATED: using any key other than quoteVolume for VolumePairList is deprecated." @@ -76,42 +81,42 @@ class VolumePairList(IPairList): (self._last_refresh + self.refresh_period < datetime.now().timestamp())): self._last_refresh = int(datetime.now().timestamp()) - pairs = self._gen_pair_whitelist(pairlist, tickers, - self._config['stake_currency'], - self._sort_key, self._min_value) + pairs = self._gen_pair_whitelist(pairlist, tickers) else: pairs = pairlist + self.log_on_refresh(logger.info, f"Searching {self._number_pairs} pairs: {pairs}") + return pairs - def _gen_pair_whitelist(self, pairlist: List[str], tickers: Dict, - base_currency: str, key: str, min_val: int) -> List[str]: + def _gen_pair_whitelist(self, pairlist: List[str], tickers: Dict) -> List[str]: """ Updates the whitelist with with a dynamically generated list - :param base_currency: base currency as str - :param key: sort key (defaults to 'quoteVolume') + :param pairlist: pairlist to filter or sort :param tickers: Tickers (from exchange.get_tickers()). :return: List of pairs """ if self._pairlist_pos == 0: # If VolumePairList is the first in the list, use fresh pairlist # Check if pair quote currency equals to the stake currency. - filtered_tickers = [v for k, v in tickers.items() - if (self._exchange.get_pair_quote_currency(k) == base_currency - and v[key] is not None)] + filtered_tickers = [ + v for k, v in tickers.items() + if (self._exchange.get_pair_quote_currency(k) == self._stake_currency + and v[self._sort_key] is not None)] else: - # If other pairlist is in front, use the incomming pairlist. + # If other pairlist is in front, use the incoming pairlist. filtered_tickers = [v for k, v in tickers.items() if k in pairlist] - if min_val > 0: - filtered_tickers = list(filter(lambda t: t[key] > min_val, filtered_tickers)) + if self._min_value > 0: + filtered_tickers = [ + v for v in filtered_tickers if v[self._sort_key] > self._min_value] - sorted_tickers = sorted(filtered_tickers, reverse=True, key=lambda t: t[key]) + sorted_tickers = sorted(filtered_tickers, reverse=True, key=lambda t: t[self._sort_key]) # Validate whitelist to only have active market pairs pairs = self._whitelist_for_active_markets([s['symbol'] for s in sorted_tickers]) pairs = self._verify_blacklist(pairs, aswarning=False) - # Limit to X number of pairs + # Limit pairlist to the requested number of pairs pairs = pairs[:self._number_pairs] return pairs diff --git a/freqtrade/pairlist/pairlistmanager.py b/freqtrade/pairlist/pairlistmanager.py index 163836c4d..9c8f0de1f 100644 --- a/freqtrade/pairlist/pairlistmanager.py +++ b/freqtrade/pairlist/pairlistmanager.py @@ -1,10 +1,8 @@ """ -Static List provider - -Provides lists as configured in config.json - - """ +PairList manager class +""" import logging +from copy import deepcopy from typing import Dict, List, Tuple from cachetools import TTLCache, cached @@ -83,25 +81,39 @@ class PairListManager(): """ Run pairlist through all configured pairlists. """ - - pairlist = self._whitelist.copy() - - # tickers should be cached to avoid calling the exchange on each call. + # Tickers should be cached to avoid calling the exchange on each call. tickers: Dict = {} if self._tickers_needed: tickers = self._get_cached_tickers() + # Adjust whitelist if filters are using tickers + pairlist = self._prepare_whitelist(self._whitelist.copy(), tickers) + # Process all pairlists in chain for pl in self._pairlists: pairlist = pl.filter_pairlist(pairlist, tickers) - # Validation against blacklist happens after the pairlists to ensure blacklist is respected. + # Validation against blacklist happens after the pairlists to ensure + # blacklist is respected. pairlist = IPairList.verify_blacklist(pairlist, self.blacklist, True) self._whitelist = pairlist + def _prepare_whitelist(self, pairlist: List[str], tickers) -> List[str]: + """ + Prepare sanitized pairlist for Pairlist Filters that use tickers data - remove + pairs that do not have ticker available + """ + if self._tickers_needed: + # Copy list since we're modifying this list + for p in deepcopy(pairlist): + if p not in tickers: + pairlist.remove(p) + + return pairlist + def create_pair_list(self, pairs: List[str], timeframe: str = None) -> ListPairsWithTimeframes: """ Create list of pair tuples with (pair, ticker_interval) """ - return [(pair, timeframe or self._config['ticker_interval']) for pair in pairs] + return [(pair, timeframe or self._config['ticker_interval']) for pair in pairs] \ No newline at end of file diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index ea34fd5bf..3f7f4e0e9 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -86,7 +86,7 @@ def check_migrate(engine) -> None: logger.debug(f'trying {table_back_name}') # Check for latest column - if not has_column(cols, 'fee_close_cost'): + if not has_column(cols, 'sell_order_status'): logger.info(f'Running database migration - backup available as {table_back_name}') fee_open = get_column_def(cols, 'fee_open', 'fee') @@ -113,6 +113,7 @@ def check_migrate(engine) -> None: close_profit_abs = get_column_def( cols, 'close_profit_abs', f"(amount * close_rate * (1 - {fee_close})) - {open_trade_price}") + sell_order_status = get_column_def(cols, 'sell_order_status', 'null') # Schema migration necessary engine.execute(f"alter table trades rename to {table_back_name}") @@ -131,7 +132,7 @@ def check_migrate(engine) -> None: stake_amount, amount, open_date, close_date, open_order_id, stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct, stoploss_order_id, stoploss_last_update, - max_rate, min_rate, sell_reason, strategy, + max_rate, min_rate, sell_reason, sell_order_status, strategy, ticker_interval, open_trade_price, close_profit_abs ) select id, lower(exchange), @@ -153,6 +154,7 @@ def check_migrate(engine) -> None: {initial_stop_loss_pct} initial_stop_loss_pct, {stoploss_order_id} stoploss_order_id, {stoploss_last_update} stoploss_last_update, {max_rate} max_rate, {min_rate} min_rate, {sell_reason} sell_reason, + {sell_order_status} sell_order_status, {strategy} strategy, {ticker_interval} ticker_interval, {open_trade_price} open_trade_price, {close_profit_abs} close_profit_abs from {table_back_name} @@ -228,6 +230,7 @@ class Trade(_DECL_BASE): # Lowest price reached min_rate = Column(Float, nullable=True) sell_reason = Column(String, nullable=True) + sell_order_status = Column(String, nullable=True) strategy = Column(String, nullable=True) ticker_interval = Column(Integer, nullable=True) @@ -267,6 +270,7 @@ class Trade(_DECL_BASE): 'stake_amount': round(self.stake_amount, 8), 'close_profit': self.close_profit, 'sell_reason': self.sell_reason, + 'sell_order_status': self.sell_order_status, 'stop_loss': self.stop_loss, 'stop_loss_pct': (self.stop_loss_pct * 100) if self.stop_loss_pct else None, 'initial_stop_loss': self.initial_stop_loss, @@ -370,6 +374,7 @@ class Trade(_DECL_BASE): self.close_profit_abs = self.calc_profit() self.close_date = datetime.utcnow() self.is_open = False + self.sell_order_status = 'closed' self.open_order_id = None logger.info( 'Marking %s as closed as the trade is fulfilled and found no open orders for it.', diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index d3b6b9639..21f54de50 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -186,7 +186,7 @@ class RPC: def _rpc_daily_profit( self, timescale: int, - stake_currency: str, fiat_display_currency: str) -> List[List[Any]]: + stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]: today = datetime.utcnow().date() profit_days: Dict[date, Dict] = {} @@ -206,28 +206,26 @@ class RPC: 'trades': len(trades) } - return [ - [ - key, - '{value:.8f} {symbol}'.format( - value=float(value['amount']), - symbol=stake_currency - ), - '{value:.3f} {symbol}'.format( + data = [ + { + 'date': key, + 'abs_profit': f'{float(value["amount"]):.8f}', + 'fiat_value': '{value:.3f}'.format( value=self._fiat_converter.convert_amount( value['amount'], stake_currency, fiat_display_currency ) if self._fiat_converter else 0, - symbol=fiat_display_currency ), - '{value} trade{s}'.format( - value=value['trades'], - s='' if value['trades'] < 2 else 's' - ), - ] + 'trade_count': f'{value["trades"]}', + } for key, value in profit_days.items() ] + return { + 'stake_currency': stake_currency, + 'fiat_display_currency': fiat_display_currency, + 'data': data + } def _rpc_trade_history(self, limit: int) -> Dict: """ Returns the X last trades """ @@ -547,5 +545,5 @@ class RPC: def _rpc_edge(self) -> List[Dict[str, Any]]: """ Returns information related to Edge """ if not self._freqtrade.edge: - raise RPCException(f'Edge is not enabled.') + raise RPCException('Edge is not enabled.') return self._freqtrade.edge.accepted_pairs() diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 856b8f138..dfda15a26 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -226,11 +226,15 @@ class Telegram(RPC): # Adding stoploss and stoploss percentage only if it is not None "*Stoploss:* `{stop_loss:.8f}` " + ("`({stop_loss_pct:.2f}%)`" if r['stop_loss_pct'] else ""), - - "*Open Order:* `{open_order}`" if r['open_order'] else "" ] + if r['open_order']: + if r['sell_order_status']: + lines.append("*Open Order:* `{open_order}` - `{sell_order_status}`") + else: + lines.append("*Open Order:* `{open_order}`") + # Filter empty lines using list-comprehension - messages.append("\n".join([l for l in lines if l]).format(**r)) + messages.append("\n".join([line for line in lines if line]).format(**r)) for msg in messages: self._send_msg(msg) @@ -276,14 +280,18 @@ class Telegram(RPC): stake_cur, fiat_disp_cur ) - stats_tab = tabulate(stats, - headers=[ - 'Day', - f'Profit {stake_cur}', - f'Profit {fiat_disp_cur}', - f'Trades' - ], - tablefmt='simple') + stats_tab = tabulate( + [[day['date'], + f"{day['abs_profit']} {stats['stake_currency']}", + f"{day['fiat_value']} {stats['fiat_display_currency']}", + f"{day['trade_count']} trades"] for day in stats['data']], + headers=[ + 'Day', + f'Profit {stake_cur}', + f'Profit {fiat_disp_cur}', + 'Trades', + ], + tablefmt='simple') message = f'Daily Profit over the last {timescale} days:\n
{stats_tab}'
self._send_msg(message, parse_mode=ParseMode.HTML)
except RPCException as e:
diff --git a/freqtrade/rpc/webhook.py b/freqtrade/rpc/webhook.py
index 1309663d4..322d990ee 100644
--- a/freqtrade/rpc/webhook.py
+++ b/freqtrade/rpc/webhook.py
@@ -47,9 +47,9 @@ class Webhook(RPC):
valuedict = self._config['webhook'].get('webhooksell', None)
elif msg['type'] == RPCMessageType.SELL_CANCEL_NOTIFICATION:
valuedict = self._config['webhook'].get('webhooksellcancel', None)
- elif msg['type'] in(RPCMessageType.STATUS_NOTIFICATION,
- RPCMessageType.CUSTOM_NOTIFICATION,
- RPCMessageType.WARNING_NOTIFICATION):
+ elif msg['type'] in (RPCMessageType.STATUS_NOTIFICATION,
+ RPCMessageType.CUSTOM_NOTIFICATION,
+ RPCMessageType.WARNING_NOTIFICATION):
valuedict = self._config['webhook'].get('webhookstatus', None)
else:
raise NotImplementedError('Unknown message type: {}'.format(msg['type']))
diff --git a/freqtrade/worker.py b/freqtrade/worker.py
index 55cf30d16..3f5ab734e 100755
--- a/freqtrade/worker.py
+++ b/freqtrade/worker.py
@@ -37,9 +37,7 @@ class Worker:
self._heartbeat_msg: float = 0
# Tell systemd that we completed initialization phase
- if self._sd_notify:
- logger.debug("sd_notify: READY=1")
- self._sd_notify.notify("READY=1")
+ self._notify("READY=1")
def _init(self, reconfig: bool) -> None:
"""
@@ -60,6 +58,15 @@ class Worker:
self._sd_notify = sdnotify.SystemdNotifier() if \
self._config.get('internals', {}).get('sd_notify', False) else None
+ def _notify(self, message: str) -> None:
+ """
+ Removes the need to verify in all occurances if sd_notify is enabled
+ :param message: Message to send to systemd if it's enabled.
+ """
+ if self._sd_notify:
+ logger.debug(f"sd_notify: {message}")
+ self._sd_notify.notify(message)
+
def run(self) -> None:
state = None
while True:
@@ -89,17 +96,13 @@ class Worker:
if state == State.STOPPED:
# Ping systemd watchdog before sleeping in the stopped state
- if self._sd_notify:
- logger.debug("sd_notify: WATCHDOG=1\\nSTATUS=State: STOPPED.")
- self._sd_notify.notify("WATCHDOG=1\nSTATUS=State: STOPPED.")
+ self._notify("WATCHDOG=1\nSTATUS=State: STOPPED.")
self._throttle(func=self._process_stopped, throttle_secs=self._throttle_secs)
elif state == State.RUNNING:
# Ping systemd watchdog before throttling
- if self._sd_notify:
- logger.debug("sd_notify: WATCHDOG=1\\nSTATUS=State: RUNNING.")
- self._sd_notify.notify("WATCHDOG=1\nSTATUS=State: RUNNING.")
+ self._notify("WATCHDOG=1\nSTATUS=State: RUNNING.")
self._throttle(func=self._process_running, throttle_secs=self._throttle_secs)
@@ -154,9 +157,7 @@ class Worker:
replaces it with the new instance
"""
# Tell systemd that we initiated reconfiguration
- if self._sd_notify:
- logger.debug("sd_notify: RELOADING=1")
- self._sd_notify.notify("RELOADING=1")
+ self._notify("RELOADING=1")
# Clean up current freqtrade modules
self.freqtrade.cleanup()
@@ -167,15 +168,11 @@ class Worker:
self.freqtrade.notify_status('config reloaded')
# Tell systemd that we completed reconfiguration
- if self._sd_notify:
- logger.debug("sd_notify: READY=1")
- self._sd_notify.notify("READY=1")
+ self._notify("READY=1")
def exit(self) -> None:
# Tell systemd that we are exiting now
- if self._sd_notify:
- logger.debug("sd_notify: STOPPING=1")
- self._sd_notify.notify("STOPPING=1")
+ self._notify("STOPPING=1")
if self.freqtrade:
self.freqtrade.notify_status('process died')
diff --git a/requirements-common.txt b/requirements-common.txt
index 017974c9e..a2038d95e 100644
--- a/requirements-common.txt
+++ b/requirements-common.txt
@@ -1,7 +1,7 @@
# requirements without requirements installable via conda
# mainly used for Raspberry pi installs
-ccxt==1.27.49
-SQLAlchemy==1.3.16
+ccxt==1.27.91
+SQLAlchemy==1.3.17
python-telegram-bot==12.7
arrow==0.15.6
cachetools==4.1.0
diff --git a/requirements-dev.txt b/requirements-dev.txt
index 616ca20f9..a37ac42ae 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -4,7 +4,7 @@
-r requirements-hyperopt.txt
coveralls==2.0.0
-flake8==3.7.9
+flake8==3.8.1
flake8-type-annotations==0.1.0
flake8-tidy-imports==4.1.0
mypy==0.770
diff --git a/tests/conftest.py b/tests/conftest.py
index 6eb110be0..971f7a5fa 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1705,7 +1705,7 @@ def hyperopt_results():
{
'loss': 0.4366182531161519,
'params_dict': {
- 'mfi-value': 15, 'fastd-value': 20, 'adx-value': 25, 'rsi-value': 28, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 88, 'sell-fastd-value': 97, 'sell-adx-value': 51, 'sell-rsi-value': 67, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1190, 'roi_t2': 541, 'roi_t3': 408, 'roi_p1': 0.026035863879169705, 'roi_p2': 0.12508730043628782, 'roi_p3': 0.27766427921605896, 'stoploss': -0.2562930402099556}, # noqa: E501
+ 'mfi-value': 15, 'fastd-value': 20, 'adx-value': 25, 'rsi-value': 28, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 88, 'sell-fastd-value': 97, 'sell-adx-value': 51, 'sell-rsi-value': 67, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1190, 'roi_t2': 541, 'roi_t3': 408, 'roi_p1': 0.026035863879169705, 'roi_p2': 0.12508730043628782, 'roi_p3': 0.27766427921605896, 'stoploss': -0.2562930402099556}, # noqa: E501
'params_details': {'buy': {'mfi-value': 15, 'fastd-value': 20, 'adx-value': 25, 'rsi-value': 28, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 88, 'sell-fastd-value': 97, 'sell-adx-value': 51, 'sell-rsi-value': 67, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.4287874435315165, 408: 0.15112316431545753, 949: 0.026035863879169705, 2139: 0}, 'stoploss': {'stoploss': -0.2562930402099556}}, # noqa: E501
'results_metrics': {'trade_count': 2, 'avg_profit': -1.254995, 'total_profit': -0.00125625, 'profit': -2.50999, 'duration': 3930.0}, # noqa: E501
'results_explanation': ' 2 trades. Avg profit -1.25%. Total profit -0.00125625 BTC ( -2.51Σ%). Avg duration 3930.0 min.', # noqa: E501
@@ -1716,11 +1716,12 @@ def hyperopt_results():
}, {
'loss': 20.0,
'params_dict': {
- 'mfi-value': 17, 'fastd-value': 38, 'adx-value': 48, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 96, 'sell-fastd-value': 68, 'sell-adx-value': 63, 'sell-rsi-value': 81, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal', 'roi_t1': 334, 'roi_t2': 683, 'roi_t3': 140, 'roi_p1': 0.06403981740598495, 'roi_p2': 0.055519840060645045, 'roi_p3': 0.3253712811342459, 'stoploss': -0.338070047333259}, # noqa: E501
- 'params_details': {'buy': {'mfi-value': 17, 'fastd-value': 38, 'adx-value': 48, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, # noqa: E501
- 'sell': {'sell-mfi-value': 96, 'sell-fastd-value': 68, 'sell-adx-value': 63, 'sell-rsi-value': 81, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal'}, # noqa: E501
- 'roi': {0: 0.4449309386008759, 140: 0.11955965746663, 823: 0.06403981740598495, 1157: 0}, # noqa: E501
- 'stoploss': {'stoploss': -0.338070047333259}},
+ 'mfi-value': 17, 'fastd-value': 38, 'adx-value': 48, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 96, 'sell-fastd-value': 68, 'sell-adx-value': 63, 'sell-rsi-value': 81, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal', 'roi_t1': 334, 'roi_t2': 683, 'roi_t3': 140, 'roi_p1': 0.06403981740598495, 'roi_p2': 0.055519840060645045, 'roi_p3': 0.3253712811342459, 'stoploss': -0.338070047333259}, # noqa: E501
+ 'params_details': {
+ 'buy': {'mfi-value': 17, 'fastd-value': 38, 'adx-value': 48, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, # noqa: E501
+ 'sell': {'sell-mfi-value': 96, 'sell-fastd-value': 68, 'sell-adx-value': 63, 'sell-rsi-value': 81, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal'}, # noqa: E501
+ 'roi': {0: 0.4449309386008759, 140: 0.11955965746663, 823: 0.06403981740598495, 1157: 0}, # noqa: E501
+ 'stoploss': {'stoploss': -0.338070047333259}},
'results_metrics': {'trade_count': 1, 'avg_profit': 0.12357, 'total_profit': 6.185e-05, 'profit': 0.12357, 'duration': 1200.0}, # noqa: E501
'results_explanation': ' 1 trades. Avg profit 0.12%. Total profit 0.00006185 BTC ( 0.12Σ%). Avg duration 1200.0 min.', # noqa: E501
'total_profit': 6.185e-05,
@@ -1767,8 +1768,9 @@ def hyperopt_results():
}, {
'loss': 4.713497421432944,
'params_dict': {'mfi-value': 13, 'fastd-value': 41, 'adx-value': 21, 'rsi-value': 29, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower', 'sell-mfi-value': 99, 'sell-fastd-value': 60, 'sell-adx-value': 81, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 771, 'roi_t2': 620, 'roi_t3': 145, 'roi_p1': 0.0586919200378493, 'roi_p2': 0.04984118697312542, 'roi_p3': 0.37521058680247044, 'stoploss': -0.14613268022709905}, # noqa: E501
- 'params_details': {'buy': {'mfi-value': 13, 'fastd-value': 41, 'adx-value': 21, 'rsi-value': 29, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower'}, 'sell': {'sell-mfi-value': 99, 'sell-fastd-value': 60, 'sell-adx-value': 81, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.4837436938134452, 145: 0.10853310701097472, 765: 0.0586919200378493, 1536: 0}, # noqa: E501
- 'stoploss': {'stoploss': -0.14613268022709905}}, # noqa: E501
+ 'params_details': {
+ 'buy': {'mfi-value': 13, 'fastd-value': 41, 'adx-value': 21, 'rsi-value': 29, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower'}, 'sell': {'sell-mfi-value': 99, 'sell-fastd-value': 60, 'sell-adx-value': 81, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.4837436938134452, 145: 0.10853310701097472, 765: 0.0586919200378493, 1536: 0}, # noqa: E501
+ 'stoploss': {'stoploss': -0.14613268022709905}}, # noqa: E501
'results_metrics': {'trade_count': 318, 'avg_profit': -0.39833954716981146, 'total_profit': -0.06339929, 'profit': -126.67197600000004, 'duration': 3140.377358490566}, # noqa: E501
'results_explanation': ' 318 trades. Avg profit -0.40%. Total profit -0.06339929 BTC (-126.67Σ%). Avg duration 3140.4 min.', # noqa: E501
'total_profit': -0.06339929,
diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py
index aa42950e2..7b1e9ddaa 100644
--- a/tests/exchange/test_exchange.py
+++ b/tests/exchange/test_exchange.py
@@ -517,9 +517,9 @@ def test_validate_pairs_restricted(default_conf, mocker, caplog):
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
Exchange(default_conf)
- assert log_has(f"Pair XRP/BTC is restricted for some users on this exchange."
- f"Please check if you are impacted by this restriction "
- f"on the exchange and eventually remove XRP/BTC from your whitelist.", caplog)
+ assert log_has("Pair XRP/BTC is restricted for some users on this exchange."
+ "Please check if you are impacted by this restriction "
+ "on the exchange and eventually remove XRP/BTC from your whitelist.", caplog)
def test_validate_pairs_stakecompatibility(default_conf, mocker, caplog):
diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py
index 093cbf966..019914720 100644
--- a/tests/optimize/test_backtesting.py
+++ b/tests/optimize/test_backtesting.py
@@ -555,7 +555,7 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir)
"""
Buy every xth candle - sell every other xth -2 (hold on to pairs a bit)
"""
- if metadata['pair'] in('ETH/BTC', 'LTC/BTC'):
+ if metadata['pair'] in ('ETH/BTC', 'LTC/BTC'):
multi = 20
else:
multi = 18
diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py
index cc8b9aa37..90e047954 100644
--- a/tests/optimize/test_hyperopt.py
+++ b/tests/optimize/test_hyperopt.py
@@ -820,7 +820,7 @@ def test_continue_hyperopt(mocker, default_conf, caplog):
Hyperopt(default_conf)
assert unlinkmock.call_count == 0
- assert log_has(f"Continuing on previous hyperopt results.", caplog)
+ assert log_has("Continuing on previous hyperopt results.", caplog)
def test_print_json_spaces_all(mocker, default_conf, caplog, capsys) -> None:
diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py
index f9e2893c3..61f6b4bd5 100644
--- a/tests/pairlist/test_pairlist.py
+++ b/tests/pairlist/test_pairlist.py
@@ -69,44 +69,44 @@ def test_log_on_refresh(mocker, static_pl_conf, markets, tickers):
def test_load_pairlist_noexist(mocker, markets, default_conf):
- bot = get_patched_freqtradebot(mocker, default_conf)
+ freqtrade = get_patched_freqtradebot(mocker, default_conf)
mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets))
- plm = PairListManager(bot.exchange, default_conf)
+ plm = PairListManager(freqtrade.exchange, default_conf)
with pytest.raises(OperationalException,
match=r"Impossible to load Pairlist 'NonexistingPairList'. "
r"This class does not exist or contains Python code errors."):
- PairListResolver.load_pairlist('NonexistingPairList', bot.exchange, plm,
+ PairListResolver.load_pairlist('NonexistingPairList', freqtrade.exchange, plm,
default_conf, {}, 1)
def test_refresh_market_pair_not_in_whitelist(mocker, markets, static_pl_conf):
- freqtradebot = get_patched_freqtradebot(mocker, static_pl_conf)
+ freqtrade = get_patched_freqtradebot(mocker, static_pl_conf)
mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets))
- freqtradebot.pairlists.refresh_pairlist()
+ freqtrade.pairlists.refresh_pairlist()
# List ordered by BaseVolume
whitelist = ['ETH/BTC', 'TKN/BTC']
# Ensure all except those in whitelist are removed
- assert set(whitelist) == set(freqtradebot.pairlists.whitelist)
+ assert set(whitelist) == set(freqtrade.pairlists.whitelist)
# Ensure config dict hasn't been changed
assert (static_pl_conf['exchange']['pair_whitelist'] ==
- freqtradebot.config['exchange']['pair_whitelist'])
+ freqtrade.config['exchange']['pair_whitelist'])
def test_refresh_static_pairlist(mocker, markets, static_pl_conf):
- freqtradebot = get_patched_freqtradebot(mocker, static_pl_conf)
+ freqtrade = get_patched_freqtradebot(mocker, static_pl_conf)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
exchange_has=MagicMock(return_value=True),
markets=PropertyMock(return_value=markets),
)
- freqtradebot.pairlists.refresh_pairlist()
+ freqtrade.pairlists.refresh_pairlist()
# List ordered by BaseVolume
whitelist = ['ETH/BTC', 'TKN/BTC']
# Ensure all except those in whitelist are removed
- assert set(whitelist) == set(freqtradebot.pairlists.whitelist)
- assert static_pl_conf['exchange']['pair_blacklist'] == freqtradebot.pairlists.blacklist
+ assert set(whitelist) == set(freqtrade.pairlists.whitelist)
+ assert static_pl_conf['exchange']['pair_blacklist'] == freqtrade.pairlists.blacklist
def test_refresh_pairlist_dynamic(mocker, shitcoinmarkets, tickers, whitelist_conf):
@@ -116,7 +116,7 @@ def test_refresh_pairlist_dynamic(mocker, shitcoinmarkets, tickers, whitelist_co
get_tickers=tickers,
exchange_has=MagicMock(return_value=True),
)
- bot = get_patched_freqtradebot(mocker, whitelist_conf)
+ freqtrade = get_patched_freqtradebot(mocker, whitelist_conf)
# Remock markets with shitcoinmarkets since get_patched_freqtradebot uses the markets fixture
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
@@ -124,9 +124,9 @@ def test_refresh_pairlist_dynamic(mocker, shitcoinmarkets, tickers, whitelist_co
)
# argument: use the whitelist dynamically by exchange-volume
whitelist = ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'XRP/BTC', 'HOT/BTC']
- bot.pairlists.refresh_pairlist()
+ freqtrade.pairlists.refresh_pairlist()
- assert whitelist == bot.pairlists.whitelist
+ assert whitelist == freqtrade.pairlists.whitelist
whitelist_conf['pairlists'] = [{'method': 'VolumePairList',
'config': {}
@@ -136,7 +136,7 @@ def test_refresh_pairlist_dynamic(mocker, shitcoinmarkets, tickers, whitelist_co
with pytest.raises(OperationalException,
match=r'`number_assets` not specified. Please check your configuration '
r'for "pairlist.config.number_assets"'):
- PairListManager(bot.exchange, whitelist_conf)
+ PairListManager(freqtrade.exchange, whitelist_conf)
def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf):
@@ -144,13 +144,13 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf):
'freqtrade.exchange.Exchange',
exchange_has=MagicMock(return_value=True),
)
- freqtradebot = get_patched_freqtradebot(mocker, whitelist_conf)
+ freqtrade = get_patched_freqtradebot(mocker, whitelist_conf)
mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets_empty))
# argument: use the whitelist dynamically by exchange-volume
whitelist = []
whitelist_conf['exchange']['pair_whitelist'] = []
- freqtradebot.pairlists.refresh_pairlist()
+ freqtrade.pairlists.refresh_pairlist()
pairslist = whitelist_conf['exchange']['pair_whitelist']
assert set(whitelist) == set(pairslist)
@@ -206,6 +206,7 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t
pairlists, base_currency, whitelist_result,
caplog) -> None:
whitelist_conf['pairlists'] = pairlists
+ whitelist_conf['stake_currency'] = base_currency
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
freqtrade = get_patched_freqtradebot(mocker, whitelist_conf)
@@ -215,7 +216,6 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t
markets=PropertyMock(return_value=shitcoinmarkets),
)
- freqtrade.config['stake_currency'] = base_currency
freqtrade.pairlists.refresh_pairlist()
whitelist = freqtrade.pairlists.whitelist
@@ -312,18 +312,18 @@ def test_volumepairlist_caching(mocker, markets, whitelist_conf, tickers):
exchange_has=MagicMock(return_value=True),
get_tickers=tickers
)
- bot = get_patched_freqtradebot(mocker, whitelist_conf)
- assert bot.pairlists._pairlists[0]._last_refresh == 0
+ freqtrade = get_patched_freqtradebot(mocker, whitelist_conf)
+ assert freqtrade.pairlists._pairlists[0]._last_refresh == 0
assert tickers.call_count == 0
- bot.pairlists.refresh_pairlist()
+ freqtrade.pairlists.refresh_pairlist()
assert tickers.call_count == 1
- assert bot.pairlists._pairlists[0]._last_refresh != 0
- lrf = bot.pairlists._pairlists[0]._last_refresh
- bot.pairlists.refresh_pairlist()
+ assert freqtrade.pairlists._pairlists[0]._last_refresh != 0
+ lrf = freqtrade.pairlists._pairlists[0]._last_refresh
+ freqtrade.pairlists.refresh_pairlist()
assert tickers.call_count == 1
# Time should not be updated.
- assert bot.pairlists._pairlists[0]._last_refresh == lrf
+ assert freqtrade.pairlists._pairlists[0]._last_refresh == lrf
def test_pairlistmanager_no_pairlist(mocker, markets, whitelist_conf, caplog):
diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py
index a1e6d9f26..63691dfb4 100644
--- a/tests/rpc/test_rpc.py
+++ b/tests/rpc/test_rpc.py
@@ -60,6 +60,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
'open_trade_price': ANY,
'close_rate_requested': ANY,
'sell_reason': ANY,
+ 'sell_order_status': ANY,
'min_rate': ANY,
'max_rate': ANY,
'strategy': ANY,
@@ -82,7 +83,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
} == results[0]
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate',
- MagicMock(side_effect=DependencyException(f"Pair 'ETH/BTC' not available")))
+ MagicMock(side_effect=DependencyException("Pair 'ETH/BTC' not available")))
results = rpc._rpc_trade_status()
assert isnan(results[0]['current_profit'])
assert isnan(results[0]['current_rate'])
@@ -103,6 +104,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
'open_trade_price': ANY,
'close_rate_requested': ANY,
'sell_reason': ANY,
+ 'sell_order_status': ANY,
'min_rate': ANY,
'max_rate': ANY,
'strategy': ANY,
@@ -165,7 +167,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None:
assert '-0.41% (-0.06)' == result[0][3]
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate',
- MagicMock(side_effect=DependencyException(f"Pair 'ETH/BTC' not available")))
+ MagicMock(side_effect=DependencyException("Pair 'ETH/BTC' not available")))
result, headers = rpc._rpc_status_table(default_conf['stake_currency'], 'USD')
assert 'instantly' == result[0][2]
assert 'ETH/BTC' in result[0][1]
@@ -203,16 +205,18 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee,
# Try valid data
update.message.text = '/daily 2'
days = rpc._rpc_daily_profit(7, stake_currency, fiat_display_currency)
- assert len(days) == 7
- for day in days:
+ assert len(days['data']) == 7
+ assert days['stake_currency'] == default_conf['stake_currency']
+ assert days['fiat_display_currency'] == default_conf['fiat_display_currency']
+ for day in days['data']:
# [datetime.date(2018, 1, 11), '0.00000000 BTC', '0.000 USD']
- assert (day[1] == '0.00000000 BTC' or
- day[1] == '0.00006217 BTC')
+ assert (day['abs_profit'] == '0.00000000' or
+ day['abs_profit'] == '0.00006217')
- assert (day[2] == '0.000 USD' or
- day[2] == '0.767 USD')
+ assert (day['fiat_value'] == '0.000' or
+ day['fiat_value'] == '0.767')
# ensure first day is current date
- assert str(days[0][0]) == str(datetime.utcnow().date())
+ assert str(days['data'][0]['date']) == str(datetime.utcnow().date())
# Try invalid data
with pytest.raises(RPCException, match=r'.*must be an integer greater than 0*'):
@@ -315,7 +319,7 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
# Test non-available pair
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate',
- MagicMock(side_effect=DependencyException(f"Pair 'ETH/BTC' not available")))
+ MagicMock(side_effect=DependencyException("Pair 'ETH/BTC' not available")))
stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
assert stats['trade_count'] == 2
assert stats['first_trade_date'] == 'just now'
diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py
index b953097d5..208a94c66 100644
--- a/tests/rpc/test_rpc_apiserver.py
+++ b/tests/rpc/test_rpc_apiserver.py
@@ -333,8 +333,10 @@ def test_api_daily(botclient, mocker, ticker, fee, markets):
)
rc = client_get(client, f"{BASE_URI}/daily")
assert_response(rc)
- assert len(rc.json) == 7
- assert rc.json[0][0] == str(datetime.utcnow().date())
+ assert len(rc.json['data']) == 7
+ assert rc.json['stake_currency'] == 'BTC'
+ assert rc.json['fiat_display_currency'] == 'USD'
+ assert rc.json['data'][0]['date'] == str(datetime.utcnow().date())
def test_api_trades(botclient, mocker, ticker, fee, markets):
@@ -520,6 +522,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets):
'open_rate_requested': 1.098e-05,
'open_trade_price': 0.0010025,
'sell_reason': None,
+ 'sell_order_status': None,
'strategy': 'DefaultStrategy',
'ticker_interval': 5}]
@@ -626,6 +629,7 @@ def test_api_forcebuy(botclient, mocker, fee):
'open_rate_requested': None,
'open_trade_price': 0.2460546025,
'sell_reason': None,
+ 'sell_order_status': None,
'strategy': None,
'ticker_interval': None
}
diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py
index bbc961763..b84073dcc 100644
--- a/tests/rpc/test_rpc_telegram.py
+++ b/tests/rpc/test_rpc_telegram.py
@@ -170,6 +170,7 @@ def test_status(default_conf, update, mocker, fee, ticker,) -> None:
'current_profit': -0.59,
'initial_stop_loss': 1.098e-05,
'stop_loss': 1.099e-05,
+ 'sell_order_status': None,
'initial_stop_loss_pct': -0.05,
'stop_loss_pct': -0.01,
'open_order': '(limit buy rem=0.00000000)'
diff --git a/tests/test_configuration.py b/tests/test_configuration.py
index c89f1381e..edcbe4516 100644
--- a/tests/test_configuration.py
+++ b/tests/test_configuration.py
@@ -73,7 +73,7 @@ def test_load_config_file_error(default_conf, mocker, caplog) -> None:
mocker.patch('freqtrade.configuration.load_config.open', mocker.mock_open(read_data=filedata))
mocker.patch.object(Path, "read_text", MagicMock(return_value=filedata))
- with pytest.raises(OperationalException, match=f".*Please verify the following segment.*"):
+ with pytest.raises(OperationalException, match=r".*Please verify the following segment.*"):
load_config_file('somefile')
diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py
index 5c5785ca3..9d9d133cc 100644
--- a/tests/test_freqtradebot.py
+++ b/tests/test_freqtradebot.py
@@ -1976,6 +1976,10 @@ def test_check_handle_timedout_buy_usercustom(default_conf, ticker, limit_buy_or
Trade.session.add(open_trade)
+ # Ensure default is to return empty (so not mocked yet)
+ freqtrade.check_handle_timedout()
+ assert cancel_order_mock.call_count == 0
+
# Return false - trade remains open
freqtrade.strategy.check_buy_timeout = MagicMock(return_value=False)
freqtrade.check_handle_timedout()
@@ -2106,6 +2110,9 @@ def test_check_handle_timedout_sell_usercustom(default_conf, ticker, limit_sell_
open_trade.is_open = False
Trade.session.add(open_trade)
+ # Ensure default is false
+ freqtrade.check_handle_timedout()
+ assert cancel_order_mock.call_count == 0
freqtrade.strategy.check_sell_timeout = MagicMock(return_value=False)
# Return false - No impact
@@ -2407,30 +2414,47 @@ def test_handle_cancel_buy_corder_empty(mocker, default_conf, limit_buy_order,
assert cancel_order_mock.call_count == 1
-def test_handle_cancel_sell_limit(mocker, default_conf) -> None:
- patch_RPCManager(mocker)
+def test_handle_cancel_sell_limit(mocker, default_conf, fee) -> None:
+ send_msg_mock = patch_RPCManager(mocker)
patch_exchange(mocker)
cancel_order_mock = MagicMock()
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
- cancel_order=cancel_order_mock
+ cancel_order=cancel_order_mock,
)
+ mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate', return_value=0.245441)
freqtrade = FreqtradeBot(default_conf)
- freqtrade._notify_sell_cancel = MagicMock()
- trade = MagicMock()
+ trade = Trade(
+ pair='LTC/ETH',
+ amount=2,
+ exchange='binance',
+ open_rate=0.245441,
+ open_order_id="123456",
+ open_date=arrow.utcnow().datetime,
+ fee_open=fee.return_value,
+ fee_close=fee.return_value,
+ )
order = {'remaining': 1,
'amount': 1,
'status': "open"}
reason = CANCEL_REASON['TIMEOUT']
assert freqtrade.handle_cancel_sell(trade, order, reason)
assert cancel_order_mock.call_count == 1
+ assert send_msg_mock.call_count == 1
+
+ send_msg_mock.reset_mock()
+
order['amount'] = 2
- assert (freqtrade.handle_cancel_sell(trade, order, reason)
- == CANCEL_REASON['PARTIALLY_FILLED'])
+ assert freqtrade.handle_cancel_sell(trade, order, reason) == CANCEL_REASON['PARTIALLY_FILLED']
# Assert cancel_order was not called (callcount remains unchanged)
assert cancel_order_mock.call_count == 1
+ assert send_msg_mock.call_count == 1
+ assert freqtrade.handle_cancel_sell(trade, order, reason) == CANCEL_REASON['PARTIALLY_FILLED']
+ # Message should not be iterated again
+ assert trade.sell_order_status == CANCEL_REASON['PARTIALLY_FILLED']
+ assert send_msg_mock.call_count == 1
def test_handle_cancel_sell_cancel_exception(mocker, default_conf) -> None:
@@ -3129,10 +3153,8 @@ def test_trailing_stop_loss(default_conf, limit_buy_order, fee, caplog, mocker)
caplog.set_level(logging.DEBUG)
# Sell as trailing-stop is reached
assert freqtrade.handle_trade(trade) is True
- assert log_has(
- f"ETH/BTC - HIT STOP: current price at 0.000012, "
- f"stoploss is 0.000015, "
- f"initial stoploss was at 0.000010, trade opened at 0.000011", caplog)
+ assert log_has("ETH/BTC - HIT STOP: current price at 0.000012, stoploss is 0.000015, "
+ "initial stoploss was at 0.000010, trade opened at 0.000011", caplog)
assert trade.sell_reason == SellType.TRAILING_STOP_LOSS.value
@@ -3175,8 +3197,8 @@ def test_trailing_stop_loss_positive(default_conf, limit_buy_order, fee,
}))
# stop-loss not reached, adjusted stoploss
assert freqtrade.handle_trade(trade) is False
- assert log_has(f"ETH/BTC - Using positive stoploss: 0.01 offset: 0 profit: 0.2666%", caplog)
- assert log_has(f"ETH/BTC - Adjusting stoploss...", caplog)
+ assert log_has("ETH/BTC - Using positive stoploss: 0.01 offset: 0 profit: 0.2666%", caplog)
+ assert log_has("ETH/BTC - Adjusting stoploss...", caplog)
assert trade.stop_loss == 0.0000138501
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker',
@@ -3232,9 +3254,8 @@ def test_trailing_stop_loss_offset(default_conf, limit_buy_order, fee,
}))
# stop-loss not reached, adjusted stoploss
assert freqtrade.handle_trade(trade) is False
- assert log_has(f"ETH/BTC - Using positive stoploss: 0.01 offset: 0.011 profit: 0.2666%",
- caplog)
- assert log_has(f"ETH/BTC - Adjusting stoploss...", caplog)
+ assert log_has("ETH/BTC - Using positive stoploss: 0.01 offset: 0.011 profit: 0.2666%", caplog)
+ assert log_has("ETH/BTC - Adjusting stoploss...", caplog)
assert trade.stop_loss == 0.0000138501
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker',
@@ -3298,7 +3319,7 @@ def test_tsl_only_offset_reached(default_conf, limit_buy_order, fee,
# stop-loss should not be adjusted as offset is not reached yet
assert freqtrade.handle_trade(trade) is False
- assert not log_has(f"ETH/BTC - Adjusting stoploss...", caplog)
+ assert not log_has("ETH/BTC - Adjusting stoploss...", caplog)
assert trade.stop_loss == 0.0000098910
# price rises above the offset (rises 12% when the offset is 5.5%)
@@ -3310,9 +3331,8 @@ def test_tsl_only_offset_reached(default_conf, limit_buy_order, fee,
}))
assert freqtrade.handle_trade(trade) is False
- assert log_has(f"ETH/BTC - Using positive stoploss: 0.05 offset: 0.055 profit: 0.1218%",
- caplog)
- assert log_has(f"ETH/BTC - Adjusting stoploss...", caplog)
+ assert log_has("ETH/BTC - Using positive stoploss: 0.05 offset: 0.055 profit: 0.1218%", caplog)
+ assert log_has("ETH/BTC - Adjusting stoploss...", caplog)
assert trade.stop_loss == 0.0000117705
diff --git a/tests/test_persistence.py b/tests/test_persistence.py
index 5c7686e28..25afed397 100644
--- a/tests/test_persistence.py
+++ b/tests/test_persistence.py
@@ -477,6 +477,7 @@ def test_migrate_old(mocker, default_conf, fee):
assert trade.close_rate_requested is None
assert trade.close_rate is not None
assert pytest.approx(trade.close_profit_abs) == trade.calc_profit()
+ assert trade.sell_order_status is None
def test_migrate_new(mocker, default_conf, fee, caplog):
@@ -756,6 +757,7 @@ def test_to_json(default_conf, fee):
'stake_amount': 0.001,
'close_profit': None,
'sell_reason': None,
+ 'sell_order_status': None,
'stop_loss': None,
'stop_loss_pct': None,
'initial_stop_loss': None,
@@ -810,6 +812,7 @@ def test_to_json(default_conf, fee):
'open_rate_requested': None,
'open_trade_price': 12.33075,
'sell_reason': None,
+ 'sell_order_status': None,
'strategy': None,
'ticker_interval': None}