From f11f5d17e91e05e2f2ac7729ccf0b4f29a352bd6 Mon Sep 17 00:00:00 2001 From: axel Date: Sat, 31 Jul 2021 00:05:45 -0400 Subject: [PATCH 001/100] add feature custom entry price for live --- docs/strategy-advanced.md | 29 +++++++++- freqtrade/constants.py | 2 + freqtrade/freqtradebot.py | 73 ++++++++++++++++++++++++ freqtrade/resolvers/strategy_resolver.py | 1 + freqtrade/rpc/api_server/api_schemas.py | 1 + freqtrade/rpc/rpc.py | 1 + freqtrade/strategy/interface.py | 58 +++++++++++++++++++ tests/strategy/test_interface.py | 28 +++++++++ tests/test_freqtradebot.py | 30 ++++++++++ 9 files changed, 222 insertions(+), 1 deletion(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 0704473fb..8c99f1d2e 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -357,6 +357,33 @@ See [Dataframe access](#dataframe-access) for more information about dataframe u --- +## Custom order entry price rules + +By default, freqtrade use the orderbook to automatically set an order price, you also have the option to create custom order prices based on your strategy. + +You can use this feature by setting the `use_custom_entry_price` option to `true` in config and creating a custom_entry_price function. + +### Custom order entry price exemple +``` python +from datetime import datetime, timedelta, timezone +from freqtrade.persistence import Trade + +class AwesomeStrategy(IStrategy): + + # ... populate_* methods + + def custom_entry_price(self, pair: str, current_time: datetime, + current_rate, **kwargs) -> float: + + dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=pair, + timeframe=self.timeframe) + entryprice = dataframe['bollinger_10_lowerband'].iat[-1] + + return entryprice + +``` + + ## Custom order timeout rules Simple, time-based order-timeouts can be configured either via strategy or in the configuration in the `unfilledtimeout` section. @@ -366,7 +393,7 @@ However, freqtrade also offers a custom callback for both order types, which all !!! Note Unfilled order timeouts are not relevant during backtesting or hyperopt, and are only relevant during real (live) trading. Therefore these methods are only called in these circumstances. -### Custom order timeout example +## Custom order timeout example A simple example, which applies different unfilled-timeouts depending on the price of the asset can be seen below. It applies a tight timeout for higher priced assets, while allowing more time to fill on cheap coins. diff --git a/freqtrade/constants.py b/freqtrade/constants.py index b48644c58..f118ac700 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -433,6 +433,8 @@ SCHEMA_MINIMAL_REQUIRED = [ CANCEL_REASON = { "TIMEOUT": "cancelled due to timeout", + "ENTRYPRICECHANGED": "Custom entry price changed", + "EXITPRICECHANGED": "Custom exit price changed", "PARTIALLY_FILLED_KEEP_OPEN": "partially filled - keeping order open", "PARTIALLY_FILLED": "partially filled", "FULLY_CANCELLED": "fully cancelled", diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 09aa06adf..4abc7e508 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -169,6 +169,7 @@ class FreqtradeBot(LoggingMixin): with self._sell_lock: # Check and handle any timed out open orders self.check_handle_timedout() + self.check_handle_custom_entryprice_outdated() # Protect from collisions with forcesell. # Without this, freqtrade my try to recreate stoploss_on_exchange orders @@ -480,6 +481,14 @@ class FreqtradeBot(LoggingMixin): else: # Calculate price buy_limit_requested = self.exchange.get_rate(pair, refresh=True, side="buy") + if self.config.get('use_custom_entry_price', False): + buy_rate = self.exchange.get_rate(pair, refresh=True, side="buy") + custom_entry_price = strategy_safe_wrapper(self.strategy.custom_entry_price, + default_retval=stake_amount)( + pair=pair, current_time=datetime.now(timezone.utc), + current_rate=buy_rate) + + buy_limit_requested = custom_entry_price if not buy_limit_requested: raise PricingError('Could not determine buy price.') @@ -911,6 +920,70 @@ class FreqtradeBot(LoggingMixin): order=order))): self.handle_cancel_sell(trade, order, constants.CANCEL_REASON['TIMEOUT']) + def _check_entryprice_outdated(self, side: str, order: dict) -> bool: + """ + Check if entry price is outdated by comparing it to the new prefered entry price + , and if the order is still open and price outdated + """ + #print("check_entryprice_outdated") + if self.config.get('use_custom_entry_price', False): + order_prefered_entry_price = order['price'] # order['trade'] + + #print(order) + #order_open_rate_requested = order.trade['open_rate_requested'] + #print("order_trade_object : {}".format(order['trade'])) + + # get pep from strategy data provider + pair = order['symbol'] + old_prefered_entry_price = order_prefered_entry_price + #new_prefered_entry_price = self.strategy.custom_info[pair]['pep_long'].iloc[-1] #buy_limit_requested + new_prefered_entry_price = self.strategy.entryprice + + old_prefered_entry_price_rounded = self.exchange.price_to_precision(pair, order_prefered_entry_price) + new_prefered_entry_price_rounded = self.exchange.price_to_precision(pair, new_prefered_entry_price) + + if old_prefered_entry_price_rounded != new_prefered_entry_price_rounded: + print("order['symbol']: {}".format(order['symbol'])) + print("new_prefered_entry_price: {}, old_prefered_entry_price: {}".format(new_prefered_entry_price, old_prefered_entry_price)) + print("rounded new pep: {}, rounded old pep: {}".format(new_prefered_entry_price_rounded, old_prefered_entry_price_rounded)) + print("Delta in prefered entry price, order to cancel") + return True + else: + return False + else: + return False + + def check_handle_custom_entryprice_outdated(self) -> None: + """ + Check if any orders prefered entryprice change and cancel if necessary + :return: None + """ + + for trade in Trade.get_open_order_trades(): + try: + if not trade.open_order_id: + continue + order = self.exchange.fetch_order(trade.open_order_id, trade.pair) + except (ExchangeError): + logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc()) + continue + + fully_cancelled = self.update_trade_state(trade, trade.open_order_id, order) + + # Refresh entryprice value if order is open + if (order['status'] == 'open'): + self.strategy.entryprice = strategy_safe_wrapper(self.strategy.custom_entry_price)( + pair=trade.pair, current_time=datetime.now(timezone.utc), + current_rate=trade.open_rate_requested) + + if (order['side'] == 'buy' and (order['status'] == 'open') and ( + self._check_entryprice_outdated('buy', order))): + self.handle_cancel_buy(trade, order, constants.CANCEL_REASON['ENTRYPRICECHANGED']) + + elif (order['side'] == 'sell' and (order['status'] == 'open') and ( + self._check_entryprice_outdated('sell', order))): + self.handle_cancel_sell(trade, order, constants.CANCEL_REASON['EXITPRICECHANGED']) + def cancel_all_open_orders(self) -> None: """ Cancel all orders that are currently open diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index 1239b78b3..3248ed385 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -79,6 +79,7 @@ class StrategyResolver(IResolver): ("trailing_stop_positive_offset", 0.0), ("trailing_only_offset_is_reached", None), ("use_custom_stoploss", None), + ("use_custom_entry_price", None), ("process_only_new_candles", None), ("order_types", None), ("order_time_in_force", None), diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 318762136..c66a01490 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -129,6 +129,7 @@ class ShowConfig(BaseModel): trailing_stop_positive_offset: Optional[float] trailing_only_offset_is_reached: Optional[bool] use_custom_stoploss: Optional[bool] + use_custom_entry_price: Optional[bool] timeframe: Optional[str] timeframe_ms: int timeframe_min: int diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 902975fde..2983c1dfa 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -116,6 +116,7 @@ class RPC: 'trailing_stop_positive_offset': config.get('trailing_stop_positive_offset'), 'trailing_only_offset_is_reached': config.get('trailing_only_offset_is_reached'), 'use_custom_stoploss': config.get('use_custom_stoploss'), + 'use_custom_entry_price': config.get('use_custom_entry_price'), 'bot_name': config.get('bot_name', 'freqtrade'), 'timeframe': config.get('timeframe'), 'timeframe_ms': timeframe_to_msecs(config['timeframe'] diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 6f3e047eb..af5be2711 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -69,6 +69,10 @@ class IStrategy(ABC, HyperStrategyMixin): # associated stoploss stoploss: float + # custom order price + entryprice: Optional[float] = None + exitprice: Optional[float] = None + # trailing stoploss trailing_stop: bool = False trailing_stop_positive: Optional[float] = None @@ -280,6 +284,24 @@ class IStrategy(ABC, HyperStrategyMixin): """ return self.stoploss + def custom_entry_price(self, pair: str, current_time: datetime, current_rate: float, + **kwargs) -> float: + """ + Custom entry price logic, returning the new entry price. + + For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ + + When not implemented by a strategy, returns None, orderbook is used to set entry price + Only called when use_custom_entry_price is set to True. + + :param pair: Pair that's currently analyzed + :param current_time: datetime object, containing the current datetime + :param current_rate: Rate, calculated based on pricing settings in ask_strategy. + :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. + :return float: New entry price value if provided + """ + return self.entryprice + def custom_sell(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, **kwargs) -> Optional[Union[str, bool]]: """ @@ -635,6 +657,42 @@ class IStrategy(ABC, HyperStrategyMixin): # logger.debug(f"{trade.pair} - No sell signal.") return SellCheckTuple(sell_type=SellType.NONE) + def entry_price_reached(self, pair: str, current_rate: float, + current_time: datetime, low: float = None, + high: float = None, side: str = "long") -> bool: + """ + Based on current candle low ,decides if entry price was reached + :param current_rate: current rate + :param low: Low value of this candle, only set in backtesting + :param high: High value of this candle, only set in backtesting + """ + + if self.use_custom_entry_price: + entry_price_value = strategy_safe_wrapper(self.custom_entry_price, default_retval=None + )(pair=pair, + current_time=current_time, + current_rate=current_rate) + # Sanity check - error cases will return None + if side == "long": + if entry_price_value > low: + return True + else: + logger.info(f"Entry failed because entry price {entry_price_value} \ + higher than candle low in long side") + return False + + elif side == "short": + if entry_price_value < high: + return True + else: + logger.info(f"Entry failed because entry price {entry_price_value} \ + higher than candle high in short side") + return False + + else: + logger.warning("CustomEntryPrice function did not return valid entry price") + return False + def stop_loss_reached(self, current_rate: float, trade: Trade, current_time: datetime, current_profit: float, force_stoploss: float, low: float = None, diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index d8c87506c..7102c1a49 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -431,6 +431,34 @@ def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, traili strategy.custom_stoploss = original_stopvalue +@pytest.mark.parametrize( + 'current_rate, exp_custom_entry', 'expected_result', 'use_custom_entry_price', 'custom_entry' [ + # Profit, adjusted stoploss(absolute), profit for 2nd call, enable trailing, + # enable custom stoploss, expected after 1st call, expected after 2nd call + (99, 98, False, True, lambda current_rate, **kwargs: current_rate - (current_rate * 0.01)), # custom_entry_price pice - (price * 0.01) + (97.8, 98, True, True, lambda current_rate, **kwargs: current_rate - (current_rate * 0.01)), # price stayed under entry price + (97.8, 98, True, True, lambda current_rate, **kwargs: current_rate + (current_rate * 0.01)), # entry price over current price + (99.9, 98, True, False, None), # feature not activated + ]) +def test_entry_price_reached(default_conf, current_rate, exp_custom_entry, candle_ohlc, + expected_result, use_custom_entry_price, custom_entry) -> None: + + default_conf.update({'strategy': 'DefaultStrategy'}) + + + strategy = StrategyResolver.load_strategy(default_conf) + + strategy.use_custom_entry_price = use_custom_entry_price + custom_entry_price = custom_entry + if use_custom_entry_price: + strategy.custom_entry_price = custom_entry(current_rate) + + now = arrow.utcnow().datetime + entry_flag = strategy.entry_price_reached(current_rate=current_rate, low= None, high=None) + + + pass + def test_custom_sell(default_conf, fee, caplog) -> None: default_conf.update({'strategy': 'DefaultStrategy'}) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index b1e02a99b..f6b8f5544 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -904,6 +904,36 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order, limit_buy_order with pytest.raises(PricingError, match="Could not determine buy price."): freqtrade.execute_buy(pair, stake_amount) +def test_execute_buy_custom_entry_price(mocker, default_conf, fee, limit_buy_order, limit_buy_order_open) -> None: + patch_RPCManager(mocker) + patch_exchange(mocker) + default_conf.update({'use_custom_entry_price': True}) + freqtrade = FreqtradeBot(default_conf) + freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=False) + stake_amount = 3 + bid = 2304 + buy_rate_mock = MagicMock(return_value=bid) + buy_mm = MagicMock(return_value=limit_buy_order_open) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_rate=buy_rate_mock, + fetch_ticker=MagicMock(return_value={ + 'bid': 2304, + 'ask': 0.00001173, + 'last': 2304 + }), + buy=buy_mm, + get_min_pair_stake_amount=MagicMock(return_value=1), + get_fee=fee, + ) + pair = 'ETH/USDT' + + # Test calling with custom entry price option activated + limit_buy_order_open['id'] = '55' + assert freqtrade.execute_buy(pair, stake_amount) + # Make sure get_rate called to provide current_rate param to custom_entry_price + assert buy_rate_mock.call_count == 1 + def test_execute_buy_confirm_error(mocker, default_conf, fee, limit_buy_order) -> None: freqtrade = get_patched_freqtradebot(mocker, default_conf) From 5284112b69828b91d42a398c2d6cad9c1a151121 Mon Sep 17 00:00:00 2001 From: axel Date: Sun, 1 Aug 2021 02:09:59 -0400 Subject: [PATCH 002/100] fix in custom entry function output,remove changes related to outdated prices, doc exemple minor changes --- docs/strategy-advanced.md | 4 +- freqtrade/freqtradebot.py | 65 --------------------------------- freqtrade/strategy/interface.py | 10 ++--- 3 files changed, 5 insertions(+), 74 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 8c99f1d2e..a8e54bbcf 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -373,7 +373,7 @@ class AwesomeStrategy(IStrategy): # ... populate_* methods def custom_entry_price(self, pair: str, current_time: datetime, - current_rate, **kwargs) -> float: + proposed_rate, **kwargs) -> float: dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=pair, timeframe=self.timeframe) @@ -393,7 +393,7 @@ However, freqtrade also offers a custom callback for both order types, which all !!! Note Unfilled order timeouts are not relevant during backtesting or hyperopt, and are only relevant during real (live) trading. Therefore these methods are only called in these circumstances. -## Custom order timeout example +### Custom order timeout example A simple example, which applies different unfilled-timeouts depending on the price of the asset can be seen below. It applies a tight timeout for higher priced assets, while allowing more time to fill on cheap coins. diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 4abc7e508..c8e930a36 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -169,7 +169,6 @@ class FreqtradeBot(LoggingMixin): with self._sell_lock: # Check and handle any timed out open orders self.check_handle_timedout() - self.check_handle_custom_entryprice_outdated() # Protect from collisions with forcesell. # Without this, freqtrade my try to recreate stoploss_on_exchange orders @@ -920,70 +919,6 @@ class FreqtradeBot(LoggingMixin): order=order))): self.handle_cancel_sell(trade, order, constants.CANCEL_REASON['TIMEOUT']) - def _check_entryprice_outdated(self, side: str, order: dict) -> bool: - """ - Check if entry price is outdated by comparing it to the new prefered entry price - , and if the order is still open and price outdated - """ - #print("check_entryprice_outdated") - if self.config.get('use_custom_entry_price', False): - order_prefered_entry_price = order['price'] # order['trade'] - - #print(order) - #order_open_rate_requested = order.trade['open_rate_requested'] - #print("order_trade_object : {}".format(order['trade'])) - - # get pep from strategy data provider - pair = order['symbol'] - old_prefered_entry_price = order_prefered_entry_price - #new_prefered_entry_price = self.strategy.custom_info[pair]['pep_long'].iloc[-1] #buy_limit_requested - new_prefered_entry_price = self.strategy.entryprice - - old_prefered_entry_price_rounded = self.exchange.price_to_precision(pair, order_prefered_entry_price) - new_prefered_entry_price_rounded = self.exchange.price_to_precision(pair, new_prefered_entry_price) - - if old_prefered_entry_price_rounded != new_prefered_entry_price_rounded: - print("order['symbol']: {}".format(order['symbol'])) - print("new_prefered_entry_price: {}, old_prefered_entry_price: {}".format(new_prefered_entry_price, old_prefered_entry_price)) - print("rounded new pep: {}, rounded old pep: {}".format(new_prefered_entry_price_rounded, old_prefered_entry_price_rounded)) - print("Delta in prefered entry price, order to cancel") - return True - else: - return False - else: - return False - - def check_handle_custom_entryprice_outdated(self) -> None: - """ - Check if any orders prefered entryprice change and cancel if necessary - :return: None - """ - - for trade in Trade.get_open_order_trades(): - try: - if not trade.open_order_id: - continue - order = self.exchange.fetch_order(trade.open_order_id, trade.pair) - except (ExchangeError): - logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc()) - continue - - fully_cancelled = self.update_trade_state(trade, trade.open_order_id, order) - - # Refresh entryprice value if order is open - if (order['status'] == 'open'): - self.strategy.entryprice = strategy_safe_wrapper(self.strategy.custom_entry_price)( - pair=trade.pair, current_time=datetime.now(timezone.utc), - current_rate=trade.open_rate_requested) - - if (order['side'] == 'buy' and (order['status'] == 'open') and ( - self._check_entryprice_outdated('buy', order))): - self.handle_cancel_buy(trade, order, constants.CANCEL_REASON['ENTRYPRICECHANGED']) - - elif (order['side'] == 'sell' and (order['status'] == 'open') and ( - self._check_entryprice_outdated('sell', order))): - self.handle_cancel_sell(trade, order, constants.CANCEL_REASON['EXITPRICECHANGED']) - def cancel_all_open_orders(self) -> None: """ Cancel all orders that are currently open diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index af5be2711..401934f7a 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -69,10 +69,6 @@ class IStrategy(ABC, HyperStrategyMixin): # associated stoploss stoploss: float - # custom order price - entryprice: Optional[float] = None - exitprice: Optional[float] = None - # trailing stoploss trailing_stop: bool = False trailing_stop_positive: Optional[float] = None @@ -284,7 +280,7 @@ class IStrategy(ABC, HyperStrategyMixin): """ return self.stoploss - def custom_entry_price(self, pair: str, current_time: datetime, current_rate: float, + def custom_entry_price(self, pair: str, current_time: datetime, proposed_rate: float, **kwargs) -> float: """ Custom entry price logic, returning the new entry price. @@ -296,11 +292,11 @@ class IStrategy(ABC, HyperStrategyMixin): :param pair: Pair that's currently analyzed :param current_time: datetime object, containing the current datetime - :param current_rate: Rate, calculated based on pricing settings in ask_strategy. + :param proposed_rate: Rate, calculated based on pricing settings in ask_strategy. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. :return float: New entry price value if provided """ - return self.entryprice + return proposed_rate def custom_sell(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, **kwargs) -> Optional[Union[str, bool]]: From 3d8c3ffd38dc8f08edf91e11ba19d57ac6765125 Mon Sep 17 00:00:00 2001 From: axel Date: Sun, 1 Aug 2021 02:21:23 -0400 Subject: [PATCH 003/100] fix syntax error in unit test --- tests/strategy/test_interface.py | 41 ++++++++++---------------------- 1 file changed, 13 insertions(+), 28 deletions(-) diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 7102c1a49..f973c5bf7 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -38,20 +38,15 @@ def test_returns_latest_signal(mocker, default_conf, ohlcv_history): mocked_history['buy'] = 0 mocked_history.loc[1, 'sell'] = 1 - assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_history) == (False, True, None) + assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_history) == (False, True) mocked_history.loc[1, 'sell'] = 0 mocked_history.loc[1, 'buy'] = 1 - assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_history) == (True, False, None) + assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_history) == (True, False) mocked_history.loc[1, 'sell'] = 0 mocked_history.loc[1, 'buy'] = 0 - assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_history) == (False, False, None) - mocked_history.loc[1, 'sell'] = 0 - mocked_history.loc[1, 'buy'] = 1 - mocked_history.loc[1, 'buy_tag'] = 'buy_signal_01' - - assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_history) == (True, False, 'buy_signal_01') + assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_history) == (False, False) def test_analyze_pair_empty(default_conf, mocker, caplog, ohlcv_history): @@ -68,21 +63,15 @@ def test_analyze_pair_empty(default_conf, mocker, caplog, ohlcv_history): def test_get_signal_empty(default_conf, mocker, caplog): - assert (False, False, None) == _STRATEGY.get_signal( - 'foo', default_conf['timeframe'], DataFrame() - ) + assert (False, False) == _STRATEGY.get_signal('foo', default_conf['timeframe'], DataFrame()) assert log_has('Empty candle (OHLCV) data for pair foo', caplog) caplog.clear() - assert (False, False, None) == _STRATEGY.get_signal('bar', default_conf['timeframe'], None) + assert (False, False) == _STRATEGY.get_signal('bar', default_conf['timeframe'], None) assert log_has('Empty candle (OHLCV) data for pair bar', caplog) caplog.clear() - assert (False, False, None) == _STRATEGY.get_signal( - 'baz', - default_conf['timeframe'], - DataFrame([]) - ) + assert (False, False) == _STRATEGY.get_signal('baz', default_conf['timeframe'], DataFrame([])) assert log_has('Empty candle (OHLCV) data for pair baz', caplog) @@ -118,11 +107,7 @@ def test_get_signal_old_dataframe(default_conf, mocker, caplog, ohlcv_history): caplog.set_level(logging.INFO) mocker.patch.object(_STRATEGY, 'assert_df') - assert (False, False, None) == _STRATEGY.get_signal( - 'xyz', - default_conf['timeframe'], - mocked_history - ) + assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['timeframe'], mocked_history) assert log_has('Outdated history for pair xyz. Last tick is 16 minutes old', caplog) @@ -432,13 +417,13 @@ def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, traili @pytest.mark.parametrize( - 'current_rate, exp_custom_entry', 'expected_result', 'use_custom_entry_price', 'custom_entry' [ - # Profit, adjusted stoploss(absolute), profit for 2nd call, enable trailing, + 'current_rate, expected_result, use_custom_entry_price, custom_entry', [ + # current rate, expected result value, profit for 2nd call, enable trailing, # enable custom stoploss, expected after 1st call, expected after 2nd call - (99, 98, False, True, lambda current_rate, **kwargs: current_rate - (current_rate * 0.01)), # custom_entry_price pice - (price * 0.01) - (97.8, 98, True, True, lambda current_rate, **kwargs: current_rate - (current_rate * 0.01)), # price stayed under entry price - (97.8, 98, True, True, lambda current_rate, **kwargs: current_rate + (current_rate * 0.01)), # entry price over current price - (99.9, 98, True, False, None), # feature not activated + (99, False, True, lambda current_rate, **kwargs: current_rate - (current_rate * 0.01)), # custom_entry_price pice - (price * 0.01) + (97.8, True, True, lambda current_rate, **kwargs: current_rate - (current_rate * 0.01)), # price stayed under entry price + (97.8, True, True, lambda current_rate, **kwargs: current_rate + (current_rate * 0.01)), # entry price over current price + (99.9, True, False, None), # feature not activated ]) def test_entry_price_reached(default_conf, current_rate, exp_custom_entry, candle_ohlc, expected_result, use_custom_entry_price, custom_entry) -> None: From d9c9b7d7fc6449d60c451ff8e3eb72726e16a9ea Mon Sep 17 00:00:00 2001 From: axel Date: Tue, 3 Aug 2021 16:02:10 -0400 Subject: [PATCH 004/100] restore interface test file --- tests/strategy/test_interface.py | 57 ++++++++++++-------------------- 1 file changed, 22 insertions(+), 35 deletions(-) diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index f973c5bf7..d8c87506c 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -38,15 +38,20 @@ def test_returns_latest_signal(mocker, default_conf, ohlcv_history): mocked_history['buy'] = 0 mocked_history.loc[1, 'sell'] = 1 - assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_history) == (False, True) + assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_history) == (False, True, None) mocked_history.loc[1, 'sell'] = 0 mocked_history.loc[1, 'buy'] = 1 - assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_history) == (True, False) + assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_history) == (True, False, None) mocked_history.loc[1, 'sell'] = 0 mocked_history.loc[1, 'buy'] = 0 - assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_history) == (False, False) + assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_history) == (False, False, None) + mocked_history.loc[1, 'sell'] = 0 + mocked_history.loc[1, 'buy'] = 1 + mocked_history.loc[1, 'buy_tag'] = 'buy_signal_01' + + assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_history) == (True, False, 'buy_signal_01') def test_analyze_pair_empty(default_conf, mocker, caplog, ohlcv_history): @@ -63,15 +68,21 @@ def test_analyze_pair_empty(default_conf, mocker, caplog, ohlcv_history): def test_get_signal_empty(default_conf, mocker, caplog): - assert (False, False) == _STRATEGY.get_signal('foo', default_conf['timeframe'], DataFrame()) + assert (False, False, None) == _STRATEGY.get_signal( + 'foo', default_conf['timeframe'], DataFrame() + ) assert log_has('Empty candle (OHLCV) data for pair foo', caplog) caplog.clear() - assert (False, False) == _STRATEGY.get_signal('bar', default_conf['timeframe'], None) + assert (False, False, None) == _STRATEGY.get_signal('bar', default_conf['timeframe'], None) assert log_has('Empty candle (OHLCV) data for pair bar', caplog) caplog.clear() - assert (False, False) == _STRATEGY.get_signal('baz', default_conf['timeframe'], DataFrame([])) + assert (False, False, None) == _STRATEGY.get_signal( + 'baz', + default_conf['timeframe'], + DataFrame([]) + ) assert log_has('Empty candle (OHLCV) data for pair baz', caplog) @@ -107,7 +118,11 @@ def test_get_signal_old_dataframe(default_conf, mocker, caplog, ohlcv_history): caplog.set_level(logging.INFO) mocker.patch.object(_STRATEGY, 'assert_df') - assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['timeframe'], mocked_history) + assert (False, False, None) == _STRATEGY.get_signal( + 'xyz', + default_conf['timeframe'], + mocked_history + ) assert log_has('Outdated history for pair xyz. Last tick is 16 minutes old', caplog) @@ -416,34 +431,6 @@ def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, traili strategy.custom_stoploss = original_stopvalue -@pytest.mark.parametrize( - 'current_rate, expected_result, use_custom_entry_price, custom_entry', [ - # current rate, expected result value, profit for 2nd call, enable trailing, - # enable custom stoploss, expected after 1st call, expected after 2nd call - (99, False, True, lambda current_rate, **kwargs: current_rate - (current_rate * 0.01)), # custom_entry_price pice - (price * 0.01) - (97.8, True, True, lambda current_rate, **kwargs: current_rate - (current_rate * 0.01)), # price stayed under entry price - (97.8, True, True, lambda current_rate, **kwargs: current_rate + (current_rate * 0.01)), # entry price over current price - (99.9, True, False, None), # feature not activated - ]) -def test_entry_price_reached(default_conf, current_rate, exp_custom_entry, candle_ohlc, - expected_result, use_custom_entry_price, custom_entry) -> None: - - default_conf.update({'strategy': 'DefaultStrategy'}) - - - strategy = StrategyResolver.load_strategy(default_conf) - - strategy.use_custom_entry_price = use_custom_entry_price - custom_entry_price = custom_entry - if use_custom_entry_price: - strategy.custom_entry_price = custom_entry(current_rate) - - now = arrow.utcnow().datetime - entry_flag = strategy.entry_price_reached(current_rate=current_rate, low= None, high=None) - - - pass - def test_custom_sell(default_conf, fee, caplog) -> None: default_conf.update({'strategy': 'DefaultStrategy'}) From 53fb8b05e72587d334e1a5865a9a6773b523edae Mon Sep 17 00:00:00 2001 From: axel Date: Tue, 3 Aug 2021 16:19:29 -0400 Subject: [PATCH 005/100] remove short logic in entry_price_reached function --- freqtrade/strategy/interface.py | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 401934f7a..366ae3504 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -281,7 +281,7 @@ class IStrategy(ABC, HyperStrategyMixin): return self.stoploss def custom_entry_price(self, pair: str, current_time: datetime, proposed_rate: float, - **kwargs) -> float: + **kwargs) -> float: """ Custom entry price logic, returning the new entry price. @@ -654,8 +654,8 @@ class IStrategy(ABC, HyperStrategyMixin): return SellCheckTuple(sell_type=SellType.NONE) def entry_price_reached(self, pair: str, current_rate: float, - current_time: datetime, low: float = None, - high: float = None, side: str = "long") -> bool: + current_time: datetime, low: float = None, + high: float = None) -> bool: """ Based on current candle low ,decides if entry price was reached :param current_rate: current rate @@ -668,23 +668,12 @@ class IStrategy(ABC, HyperStrategyMixin): )(pair=pair, current_time=current_time, current_rate=current_rate) - # Sanity check - error cases will return None - if side == "long": + + if entry_price_value is not None: if entry_price_value > low: return True else: - logger.info(f"Entry failed because entry price {entry_price_value} \ - higher than candle low in long side") return False - - elif side == "short": - if entry_price_value < high: - return True - else: - logger.info(f"Entry failed because entry price {entry_price_value} \ - higher than candle high in short side") - return False - else: logger.warning("CustomEntryPrice function did not return valid entry price") return False From 00939b63f2bb05394af2023694b30b15be73ceef Mon Sep 17 00:00:00 2001 From: axel Date: Tue, 3 Aug 2021 16:25:18 -0400 Subject: [PATCH 006/100] flake 8 fixes --- freqtrade/freqtradebot.py | 2 +- freqtrade/strategy/interface.py | 8 ++++---- tests/test_freqtradebot.py | 3 ++- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index c8e930a36..5b60c0ea3 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -483,7 +483,7 @@ class FreqtradeBot(LoggingMixin): if self.config.get('use_custom_entry_price', False): buy_rate = self.exchange.get_rate(pair, refresh=True, side="buy") custom_entry_price = strategy_safe_wrapper(self.strategy.custom_entry_price, - default_retval=stake_amount)( + default_retval=stake_amount)( pair=pair, current_time=datetime.now(timezone.utc), current_rate=buy_rate) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 366ae3504..750f6f39b 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -664,10 +664,10 @@ class IStrategy(ABC, HyperStrategyMixin): """ if self.use_custom_entry_price: - entry_price_value = strategy_safe_wrapper(self.custom_entry_price, default_retval=None - )(pair=pair, - current_time=current_time, - current_rate=current_rate) + entry_price_value = strategy_safe_wrapper(self.custom_entry_price, default_retval=None)( + pair=pair, + current_time=current_time, + current_rate=current_rate) if entry_price_value is not None: if entry_price_value > low: diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index f6b8f5544..6aaa17094 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -904,7 +904,8 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order, limit_buy_order with pytest.raises(PricingError, match="Could not determine buy price."): freqtrade.execute_buy(pair, stake_amount) -def test_execute_buy_custom_entry_price(mocker, default_conf, fee, limit_buy_order, limit_buy_order_open) -> None: + +def test_execute_buy_custom_entry_price(mocker, default_conf, fee, limit_buy_order_open) -> None: patch_RPCManager(mocker) patch_exchange(mocker) default_conf.update({'use_custom_entry_price': True}) From 42e24d8b4b76a4e850c8b4687166f0bc79b82d22 Mon Sep 17 00:00:00 2001 From: axel Date: Tue, 3 Aug 2021 16:35:16 -0400 Subject: [PATCH 007/100] remove price change cancel reason in contants, will be added in another PR --- freqtrade/constants.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index f118ac700..b48644c58 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -433,8 +433,6 @@ SCHEMA_MINIMAL_REQUIRED = [ CANCEL_REASON = { "TIMEOUT": "cancelled due to timeout", - "ENTRYPRICECHANGED": "Custom entry price changed", - "EXITPRICECHANGED": "Custom exit price changed", "PARTIALLY_FILLED_KEEP_OPEN": "partially filled - keeping order open", "PARTIALLY_FILLED": "partially filled", "FULLY_CANCELLED": "fully cancelled", From 16146357b329a759654c302e391e304848adc951 Mon Sep 17 00:00:00 2001 From: axel Date: Tue, 3 Aug 2021 16:39:35 -0400 Subject: [PATCH 008/100] reuse buy_limit_requested as rate input for custom entry price --- freqtrade/freqtradebot.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 5b60c0ea3..ffe223899 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -481,11 +481,10 @@ class FreqtradeBot(LoggingMixin): # Calculate price buy_limit_requested = self.exchange.get_rate(pair, refresh=True, side="buy") if self.config.get('use_custom_entry_price', False): - buy_rate = self.exchange.get_rate(pair, refresh=True, side="buy") custom_entry_price = strategy_safe_wrapper(self.strategy.custom_entry_price, default_retval=stake_amount)( pair=pair, current_time=datetime.now(timezone.utc), - current_rate=buy_rate) + current_rate=buy_limit_requested) buy_limit_requested = custom_entry_price From b3dafb378e32c39e6eba60e61b2acfc8ea79bf08 Mon Sep 17 00:00:00 2001 From: axel Date: Tue, 3 Aug 2021 16:54:28 -0400 Subject: [PATCH 009/100] remove use_custom_entry_price as a config option --- freqtrade/freqtradebot.py | 11 +++++------ freqtrade/resolvers/strategy_resolver.py | 1 - freqtrade/rpc/api_server/api_schemas.py | 1 - freqtrade/rpc/rpc.py | 1 - freqtrade/strategy/interface.py | 23 ++++++++++------------- tests/test_freqtradebot.py | 1 - 6 files changed, 15 insertions(+), 23 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index ffe223899..ad8d0b9c4 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -480,13 +480,12 @@ class FreqtradeBot(LoggingMixin): else: # Calculate price buy_limit_requested = self.exchange.get_rate(pair, refresh=True, side="buy") - if self.config.get('use_custom_entry_price', False): - custom_entry_price = strategy_safe_wrapper(self.strategy.custom_entry_price, - default_retval=stake_amount)( - pair=pair, current_time=datetime.now(timezone.utc), - current_rate=buy_limit_requested) + custom_entry_price = strategy_safe_wrapper(self.strategy.custom_entry_price, + default_retval=stake_amount)( + pair=pair, current_time=datetime.now(timezone.utc), + current_rate=buy_limit_requested) - buy_limit_requested = custom_entry_price + buy_limit_requested = custom_entry_price if not buy_limit_requested: raise PricingError('Could not determine buy price.') diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index 3248ed385..1239b78b3 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -79,7 +79,6 @@ class StrategyResolver(IResolver): ("trailing_stop_positive_offset", 0.0), ("trailing_only_offset_is_reached", None), ("use_custom_stoploss", None), - ("use_custom_entry_price", None), ("process_only_new_candles", None), ("order_types", None), ("order_time_in_force", None), diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index c66a01490..318762136 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -129,7 +129,6 @@ class ShowConfig(BaseModel): trailing_stop_positive_offset: Optional[float] trailing_only_offset_is_reached: Optional[bool] use_custom_stoploss: Optional[bool] - use_custom_entry_price: Optional[bool] timeframe: Optional[str] timeframe_ms: int timeframe_min: int diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 2983c1dfa..902975fde 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -116,7 +116,6 @@ class RPC: 'trailing_stop_positive_offset': config.get('trailing_stop_positive_offset'), 'trailing_only_offset_is_reached': config.get('trailing_only_offset_is_reached'), 'use_custom_stoploss': config.get('use_custom_stoploss'), - 'use_custom_entry_price': config.get('use_custom_entry_price'), 'bot_name': config.get('bot_name', 'freqtrade'), 'timeframe': config.get('timeframe'), 'timeframe_ms': timeframe_to_msecs(config['timeframe'] diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 750f6f39b..d04524687 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -288,7 +288,6 @@ class IStrategy(ABC, HyperStrategyMixin): For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ When not implemented by a strategy, returns None, orderbook is used to set entry price - Only called when use_custom_entry_price is set to True. :param pair: Pair that's currently analyzed :param current_time: datetime object, containing the current datetime @@ -662,21 +661,19 @@ class IStrategy(ABC, HyperStrategyMixin): :param low: Low value of this candle, only set in backtesting :param high: High value of this candle, only set in backtesting """ + entry_price_value = strategy_safe_wrapper(self.custom_entry_price, default_retval=None)( + pair=pair, + current_time=current_time, + current_rate=current_rate) - if self.use_custom_entry_price: - entry_price_value = strategy_safe_wrapper(self.custom_entry_price, default_retval=None)( - pair=pair, - current_time=current_time, - current_rate=current_rate) - - if entry_price_value is not None: - if entry_price_value > low: - return True - else: - return False + if entry_price_value is not None: + if entry_price_value > low: + return True else: - logger.warning("CustomEntryPrice function did not return valid entry price") return False + else: + logger.warning("CustomEntryPrice function did not return valid entry price") + return False def stop_loss_reached(self, current_rate: float, trade: Trade, current_time: datetime, current_profit: float, diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 6aaa17094..08e02cffb 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -908,7 +908,6 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order, limit_buy_order def test_execute_buy_custom_entry_price(mocker, default_conf, fee, limit_buy_order_open) -> None: patch_RPCManager(mocker) patch_exchange(mocker) - default_conf.update({'use_custom_entry_price': True}) freqtrade = FreqtradeBot(default_conf) freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=False) stake_amount = 3 From b644233eada98d37b435a38f4fc69cecdda3c451 Mon Sep 17 00:00:00 2001 From: axel Date: Tue, 3 Aug 2021 17:03:27 -0400 Subject: [PATCH 010/100] rename custom_entry_price kwarg to align it to the interface --- freqtrade/freqtradebot.py | 2 +- freqtrade/strategy/interface.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index ad8d0b9c4..2592ccc91 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -483,7 +483,7 @@ class FreqtradeBot(LoggingMixin): custom_entry_price = strategy_safe_wrapper(self.strategy.custom_entry_price, default_retval=stake_amount)( pair=pair, current_time=datetime.now(timezone.utc), - current_rate=buy_limit_requested) + proposed_rate=buy_limit_requested) buy_limit_requested = custom_entry_price diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index d04524687..1cbc334f0 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -664,7 +664,7 @@ class IStrategy(ABC, HyperStrategyMixin): entry_price_value = strategy_safe_wrapper(self.custom_entry_price, default_retval=None)( pair=pair, current_time=current_time, - current_rate=current_rate) + proposed_rate=current_rate) if entry_price_value is not None: if entry_price_value > low: From bc3e6deb1c1efa5c9c3120f4c2d07c33bff5ef47 Mon Sep 17 00:00:00 2001 From: axel Date: Tue, 3 Aug 2021 17:09:52 -0400 Subject: [PATCH 011/100] remove specific test for buy with custom entry --- tests/test_freqtradebot.py | 30 ------------------------------ 1 file changed, 30 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 08e02cffb..b1e02a99b 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -905,36 +905,6 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order, limit_buy_order freqtrade.execute_buy(pair, stake_amount) -def test_execute_buy_custom_entry_price(mocker, default_conf, fee, limit_buy_order_open) -> None: - patch_RPCManager(mocker) - patch_exchange(mocker) - freqtrade = FreqtradeBot(default_conf) - freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=False) - stake_amount = 3 - bid = 2304 - buy_rate_mock = MagicMock(return_value=bid) - buy_mm = MagicMock(return_value=limit_buy_order_open) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - get_rate=buy_rate_mock, - fetch_ticker=MagicMock(return_value={ - 'bid': 2304, - 'ask': 0.00001173, - 'last': 2304 - }), - buy=buy_mm, - get_min_pair_stake_amount=MagicMock(return_value=1), - get_fee=fee, - ) - pair = 'ETH/USDT' - - # Test calling with custom entry price option activated - limit_buy_order_open['id'] = '55' - assert freqtrade.execute_buy(pair, stake_amount) - # Make sure get_rate called to provide current_rate param to custom_entry_price - assert buy_rate_mock.call_count == 1 - - def test_execute_buy_confirm_error(mocker, default_conf, fee, limit_buy_order) -> None: freqtrade = get_patched_freqtradebot(mocker, default_conf) mocker.patch.multiple( From 2cf781f3dd8a0c87ec08b0ff08ebb2a5b63d6786 Mon Sep 17 00:00:00 2001 From: axel Date: Wed, 4 Aug 2021 18:32:39 -0400 Subject: [PATCH 012/100] add freqtradebot execute_buy test in custom entry price case --- tests/test_freqtradebot.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index b1e02a99b..7ea67162d 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -904,6 +904,15 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order, limit_buy_order with pytest.raises(PricingError, match="Could not determine buy price."): freqtrade.execute_buy(pair, stake_amount) + # In case of custom entry price + limit_buy_order['status'] = 'open' + limit_buy_order['id'] = '5566' + freqtrade.strategy.custom_entry_price = lambda **kwargs: 0.77 + assert freqtrade.execute_buy(pair, stake_amount) + trade = Trade.query.all()[6] + assert trade + assert trade.open_rate_requested == 0.77 + def test_execute_buy_confirm_error(mocker, default_conf, fee, limit_buy_order) -> None: freqtrade = get_patched_freqtradebot(mocker, default_conf) From d9b9eecd4d039cc0bc37baeb4075481b2f628b56 Mon Sep 17 00:00:00 2001 From: axel Date: Wed, 4 Aug 2021 18:47:14 -0400 Subject: [PATCH 013/100] remove entry price reached method --- freqtrade/strategy/interface.py | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 1cbc334f0..3d9ad0915 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -652,29 +652,6 @@ class IStrategy(ABC, HyperStrategyMixin): # logger.debug(f"{trade.pair} - No sell signal.") return SellCheckTuple(sell_type=SellType.NONE) - def entry_price_reached(self, pair: str, current_rate: float, - current_time: datetime, low: float = None, - high: float = None) -> bool: - """ - Based on current candle low ,decides if entry price was reached - :param current_rate: current rate - :param low: Low value of this candle, only set in backtesting - :param high: High value of this candle, only set in backtesting - """ - entry_price_value = strategy_safe_wrapper(self.custom_entry_price, default_retval=None)( - pair=pair, - current_time=current_time, - proposed_rate=current_rate) - - if entry_price_value is not None: - if entry_price_value > low: - return True - else: - return False - else: - logger.warning("CustomEntryPrice function did not return valid entry price") - return False - def stop_loss_reached(self, current_rate: float, trade: Trade, current_time: datetime, current_profit: float, force_stoploss: float, low: float = None, From f9f519fd3c55f7e2f2040ec9cf3592bc22154711 Mon Sep 17 00:00:00 2001 From: axel Date: Wed, 4 Aug 2021 18:54:17 -0400 Subject: [PATCH 014/100] add custom_exit_price function to interface --- freqtrade/strategy/interface.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 3d9ad0915..e1aba7d7a 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -297,6 +297,23 @@ class IStrategy(ABC, HyperStrategyMixin): """ return proposed_rate + def custom_exit_price(self, pair: str, current_time: datetime, proposed_rate: float, + **kwargs) -> float: + """ + Custom exit price logic, returning the new exit price. + + For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ + + When not implemented by a strategy, returns None, orderbook is used to set exit price + + :param pair: Pair that's currently analyzed + :param current_time: datetime object, containing the current datetime + :param proposed_rate: Rate, calculated based on pricing settings in ask_strategy. + :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. + :return float: New exit price value if provided + """ + return proposed_rate + def custom_sell(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, **kwargs) -> Optional[Union[str, bool]]: """ From f243ad4af0b8b1a5e8350db05f0717b16734bd27 Mon Sep 17 00:00:00 2001 From: axel Date: Wed, 4 Aug 2021 19:09:55 -0400 Subject: [PATCH 015/100] add custom_exit_price in strategy interface --- freqtrade/strategy/interface.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index e1aba7d7a..f7d0a0aae 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -297,8 +297,9 @@ class IStrategy(ABC, HyperStrategyMixin): """ return proposed_rate - def custom_exit_price(self, pair: str, current_time: datetime, proposed_rate: float, - **kwargs) -> float: + def custom_exit_price(self, pair: str, trade: Trade, + current_time: datetime, proposed_rate: float, + current_rate: float, current_profit: float, **kwargs) -> float: """ Custom exit price logic, returning the new exit price. @@ -307,8 +308,10 @@ class IStrategy(ABC, HyperStrategyMixin): When not implemented by a strategy, returns None, orderbook is used to set exit price :param pair: Pair that's currently analyzed + :param trade: trade object. :param current_time: datetime object, containing the current datetime :param proposed_rate: Rate, calculated based on pricing settings in ask_strategy. + :param current_profit: Current profit (as ratio), calculated based on current_rate. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. :return float: New exit price value if provided """ From cb3b0cf311d43583d18fe1b67878a8e1d286b834 Mon Sep 17 00:00:00 2001 From: axel Date: Wed, 4 Aug 2021 23:09:40 -0400 Subject: [PATCH 016/100] add custom_exit_price in interface and freqtradebot --- freqtrade/freqtradebot.py | 8 ++++++++ freqtrade/strategy/interface.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 2592ccc91..8d50b46e3 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1082,6 +1082,14 @@ class FreqtradeBot(LoggingMixin): and self.strategy.order_types['stoploss_on_exchange']: limit = trade.stop_loss + # set custom_exit_price if available + current_profit = trade.calc_profit_ratio(limit) + limit = strategy_safe_wrapper(self.strategy.custom_exit_price, + default_retval=limit)( + pair=trade.pair, trade=trade, + current_time=datetime.now(timezone.utc), + proposed_rate=limit, current_profit=current_profit) + # First cancelling stoploss on exchange ... if self.strategy.order_types.get('stoploss_on_exchange') and trade.stoploss_order_id: try: diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index f7d0a0aae..4c0e1c8a9 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -299,7 +299,7 @@ class IStrategy(ABC, HyperStrategyMixin): def custom_exit_price(self, pair: str, trade: Trade, current_time: datetime, proposed_rate: float, - current_rate: float, current_profit: float, **kwargs) -> float: + current_profit: float, **kwargs) -> float: """ Custom exit price logic, returning the new exit price. From 0aeebc9d53dad23f8ed2f02b0b91b11d6276cbea Mon Sep 17 00:00:00 2001 From: axel Date: Thu, 5 Aug 2021 17:57:45 -0400 Subject: [PATCH 017/100] add test for custom exit price --- tests/test_freqtradebot.py | 64 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 7ea67162d..c73e51dec 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2724,6 +2724,70 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, mocker) } == last_msg +def test_execute_sell_custom_exit_price(default_conf, ticker, fee, ticker_sell_up, mocker) -> None: + rpc_mock = patch_RPCManager(mocker) + patch_exchange(mocker) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + fetch_ticker=ticker, + get_fee=fee, + _is_dry_limit_order_filled=MagicMock(return_value=False), + ) + patch_whitelist(mocker, default_conf) + freqtrade = FreqtradeBot(default_conf) + patch_get_signal(freqtrade) + freqtrade.strategy.confirm_trade_exit = MagicMock(return_value=False) + + # Create some test data + freqtrade.enter_positions() + rpc_mock.reset_mock() + + trade = Trade.query.first() + assert trade + assert freqtrade.strategy.confirm_trade_exit.call_count == 0 + + # Increase the price and sell it + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + fetch_ticker=ticker_sell_up + ) + + freqtrade.strategy.confirm_trade_exit = MagicMock(return_value=True) + + # Set a custom exit price + freqtrade.strategy.custom_exit_price = lambda **kwargs: 1.170e-05 + + freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], + sell_reason=SellCheckTuple(sell_type=SellType.SELL_SIGNAL)) + + # Sell price must be different to default bid price + + assert freqtrade.strategy.confirm_trade_exit.call_count == 1 + + assert rpc_mock.call_count == 1 + last_msg = rpc_mock.call_args_list[-1][0][0] + assert { + 'trade_id': 1, + 'type': RPCMessageType.SELL, + 'exchange': 'Binance', + 'pair': 'ETH/BTC', + 'gain': 'profit', + 'limit': 1.170e-05, + 'amount': 91.07468123, + 'order_type': 'limit', + 'open_rate': 1.098e-05, + 'current_rate': 1.173e-05, + 'profit_amount': 6.041e-05, + 'profit_ratio': 0.06025919, + 'stake_currency': 'BTC', + 'fiat_currency': 'USD', + 'sell_reason': SellType.SELL_SIGNAL.value, + 'open_date': ANY, + 'close_date': ANY, + 'close_rate': ANY, + } == last_msg + + def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fee, ticker_sell_down, mocker) -> None: rpc_mock = patch_RPCManager(mocker) From 84d082033b21cbffb02f4fd283a8bd33936733e4 Mon Sep 17 00:00:00 2001 From: axel Date: Thu, 5 Aug 2021 18:00:31 -0400 Subject: [PATCH 018/100] fix default retval for strategy custom_entry_price --- freqtrade/freqtradebot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 8d50b46e3..99f5d2894 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -481,7 +481,7 @@ class FreqtradeBot(LoggingMixin): # Calculate price buy_limit_requested = self.exchange.get_rate(pair, refresh=True, side="buy") custom_entry_price = strategy_safe_wrapper(self.strategy.custom_entry_price, - default_retval=stake_amount)( + default_retval=buy_limit_requested)( pair=pair, current_time=datetime.now(timezone.utc), proposed_rate=buy_limit_requested) From 0985b11267b890aeeccaed79ebc4ed9644c39f99 Mon Sep 17 00:00:00 2001 From: axel Date: Thu, 5 Aug 2021 18:16:16 -0400 Subject: [PATCH 019/100] add doc for custom exit price --- docs/strategy-advanced.md | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index a8e54bbcf..f59cb8ef5 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -357,13 +357,13 @@ See [Dataframe access](#dataframe-access) for more information about dataframe u --- -## Custom order entry price rules +## Custom order price rules By default, freqtrade use the orderbook to automatically set an order price, you also have the option to create custom order prices based on your strategy. -You can use this feature by setting the `use_custom_entry_price` option to `true` in config and creating a custom_entry_price function. +You can use this feature by creating a custom_entry_price function in your strategy file to customize entry prices and custom_exit_price for exits. -### Custom order entry price exemple +### Custom order entry and exit price exemple ``` python from datetime import datetime, timedelta, timezone from freqtrade.persistence import Trade @@ -373,13 +373,23 @@ class AwesomeStrategy(IStrategy): # ... populate_* methods def custom_entry_price(self, pair: str, current_time: datetime, - proposed_rate, **kwargs) -> float: + proposed_rate, **kwargs) -> float: dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=pair, timeframe=self.timeframe) - entryprice = dataframe['bollinger_10_lowerband'].iat[-1] + proposed_entryprice = dataframe['bollinger_10_lowerband'].iat[-1] - return entryprice + return proposed_entryprice + + def custom_exit_price(self, pair: str, trade: Trade, + current_time: datetime, proposed_rate: float, + current_profit: float, **kwargs) -> float: + + dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=pair, + timeframe=self.timeframe) + proposed_exitprice = dataframe['bollinger_10_upperband'].iat[-1] + + return proposed_exitprice ``` From 5393c55b513028a3159da23a19e241050766651b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 7 Aug 2021 16:01:20 +0200 Subject: [PATCH 020/100] Document min_value for VolumePairList closes #5260 --- docs/includes/pairlists.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index 995e49a2d..b92b05af9 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -58,7 +58,7 @@ This option must be configured along with `exchange.skip_pair_validation` in the When used in the chain of Pairlist Handlers in a non-leading position (after StaticPairList and other Pairlist Filters), `VolumePairList` considers outputs of previous Pairlist Handlers, adding its sorting/selection of the pairs by the trading volume. -When used on the leading position of the chain of Pairlist Handlers, it does not consider `pair_whitelist` configuration setting, but selects the top assets from all available markets (with matching stake-currency) on the exchange. +When used in the leading position of the chain of Pairlist Handlers, the `pair_whitelist` configuration setting is ignored. Instead, `VolumePairList` selects the top assets from all available markets with matching stake-currency on the exchange. The `refresh_period` setting allows to define the period (in seconds), at which the pairlist will be refreshed. Defaults to 1800s (30 minutes). The pairlist cache (`refresh_period`) on `VolumePairList` is only applicable to generating pairlists. @@ -74,11 +74,14 @@ Filtering instances (not the first position in the list) will not apply any cach "method": "VolumePairList", "number_assets": 20, "sort_key": "quoteVolume", + "min_value": 0, "refresh_period": 1800 } ], ``` +You can define a minimum volume with `min_value` - which will filter out pairs with a volume lower than the specified value in the specified timerange. + `VolumePairList` can also operate in an advanced mode to build volume over a given timerange of specified candle size. It utilizes exchange historical candle data, builds a typical price (calculated by (open+high+low)/3) and multiplies the typical price with every candle's volume. The sum is the `quoteVolume` over the given range. This allows different scenarios, for a more smoothened volume, when using longer ranges with larger candle sizes, or the opposite when using a short range with small candles. For convenience `lookback_days` can be specified, which will imply that 1d candles will be used for the lookback. In the example below the pairlist would be created based on the last 7 days: @@ -89,6 +92,7 @@ For convenience `lookback_days` can be specified, which will imply that 1d candl "method": "VolumePairList", "number_assets": 20, "sort_key": "quoteVolume", + "min_value": 0, "refresh_period": 86400, "lookback_days": 7 } From cf70b34ff0504dcbffc1106bfbefd521bc1c3357 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 7 Aug 2021 16:15:42 +0200 Subject: [PATCH 021/100] Add min_value to all volumepairlist samples --- docs/includes/pairlists.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index b92b05af9..6e23c9003 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -113,6 +113,7 @@ More sophisticated approach can be used, by using `lookback_timeframe` for candl "method": "VolumePairList", "number_assets": 20, "sort_key": "quoteVolume", + "min_value": 0, "refresh_period": 3600, "lookback_timeframe": "1h", "lookback_period": 72 From 7eaadb2630a99c8f467751bdff071b52cdb098b2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 8 Aug 2021 09:50:50 +0200 Subject: [PATCH 022/100] Add custom-* methods to bot-basics closes #5370 --- docs/bot-basics.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/bot-basics.md b/docs/bot-basics.md index 943af0362..44181abfa 100644 --- a/docs/bot-basics.md +++ b/docs/bot-basics.md @@ -35,7 +35,7 @@ By default, loop runs every few seconds (`internals.process_throttle_secs`) and * Calls `check_buy_timeout()` strategy callback for open buy orders. * Calls `check_sell_timeout()` strategy callback for open sell orders. * Verifies existing positions and eventually places sell orders. - * Considers stoploss, ROI and sell-signal. + * Considers stoploss, ROI and sell-signal, `custom_sell()` and `custom_stoploss()`. * Determine sell-price based on `ask_strategy` configuration setting. * Before a sell order is placed, `confirm_trade_exit()` strategy callback is called. * Check if trade-slots are still available (if `max_open_trades` is reached). @@ -52,9 +52,10 @@ This loop will be repeated again and again until the bot is stopped. * Load historic data for configured pairlist. * Calls `bot_loop_start()` once. * Calculate indicators (calls `populate_indicators()` once per pair). -* Calculate buy / sell signals (calls `populate_buy_trend()` and `populate_sell_trend()` once per pair) -* Confirm trade buy / sell (calls `confirm_trade_entry()` and `confirm_trade_exit()` if implemented in the strategy) +* Calculate buy / sell signals (calls `populate_buy_trend()` and `populate_sell_trend()` once per pair). * Loops per candle simulating entry and exit points. + * Confirm trade buy / sell (calls `confirm_trade_entry()` and `confirm_trade_exit()` if implemented in the strategy). + * Call `custom_stoploss()` and `custom_sell()` to find custom exit points. * Generate backtest report output !!! Note From 0ae4eccea5ad7cd773e0d0e309617bfcf6a1e4ae Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 8 Aug 2021 10:05:28 +0200 Subject: [PATCH 023/100] Refactor Hyperopt-list and hyperopt-show to reduce some duplicate code --- freqtrade/commands/hyperopt_commands.py | 180 +------------------ freqtrade/optimize/hyperopt_epoch_filters.py | 142 +++++++++++++++ freqtrade/optimize/hyperopt_tools.py | 28 ++- 3 files changed, 174 insertions(+), 176 deletions(-) create mode 100644 freqtrade/optimize/hyperopt_epoch_filters.py diff --git a/freqtrade/commands/hyperopt_commands.py b/freqtrade/commands/hyperopt_commands.py index 4694d1111..089529d15 100755 --- a/freqtrade/commands/hyperopt_commands.py +++ b/freqtrade/commands/hyperopt_commands.py @@ -1,6 +1,6 @@ import logging from operator import itemgetter -from typing import Any, Dict, List +from typing import Any, Dict from colorama import init as colorama_init @@ -28,30 +28,12 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None: no_details = config.get('hyperopt_list_no_details', False) no_header = False - filteroptions = { - 'only_best': config.get('hyperopt_list_best', False), - 'only_profitable': config.get('hyperopt_list_profitable', False), - 'filter_min_trades': config.get('hyperopt_list_min_trades', 0), - 'filter_max_trades': config.get('hyperopt_list_max_trades', 0), - 'filter_min_avg_time': config.get('hyperopt_list_min_avg_time', None), - 'filter_max_avg_time': config.get('hyperopt_list_max_avg_time', None), - 'filter_min_avg_profit': config.get('hyperopt_list_min_avg_profit', None), - 'filter_max_avg_profit': config.get('hyperopt_list_max_avg_profit', None), - 'filter_min_total_profit': config.get('hyperopt_list_min_total_profit', None), - 'filter_max_total_profit': config.get('hyperopt_list_max_total_profit', None), - 'filter_min_objective': config.get('hyperopt_list_min_objective', None), - 'filter_max_objective': config.get('hyperopt_list_max_objective', None), - } - results_file = get_latest_hyperopt_file( config['user_data_dir'] / 'hyperopt_results', config.get('hyperoptexportfilename')) # Previous evaluations - epochs = HyperoptTools.load_previous_results(results_file) - total_epochs = len(epochs) - - epochs = hyperopt_filter_epochs(epochs, filteroptions) + epochs, total_epochs = HyperoptTools.load_filtered_results(results_file, config) if print_colorized: colorama_init(autoreset=True) @@ -59,7 +41,7 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None: if not export_csv: try: print(HyperoptTools.get_result_table(config, epochs, total_epochs, - not filteroptions['only_best'], + not config.get('hyperopt_list_best', False), print_colorized, 0)) except KeyboardInterrupt: print('User interrupted..') @@ -71,7 +53,7 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None: if epochs and export_csv: HyperoptTools.export_csv_file( - config, epochs, total_epochs, not filteroptions['only_best'], export_csv + config, epochs, total_epochs, not config.get('hyperopt_list_best', False), export_csv ) @@ -91,26 +73,9 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None: n = config.get('hyperopt_show_index', -1) - filteroptions = { - 'only_best': config.get('hyperopt_list_best', False), - 'only_profitable': config.get('hyperopt_list_profitable', False), - 'filter_min_trades': config.get('hyperopt_list_min_trades', 0), - 'filter_max_trades': config.get('hyperopt_list_max_trades', 0), - 'filter_min_avg_time': config.get('hyperopt_list_min_avg_time', None), - 'filter_max_avg_time': config.get('hyperopt_list_max_avg_time', None), - 'filter_min_avg_profit': config.get('hyperopt_list_min_avg_profit', None), - 'filter_max_avg_profit': config.get('hyperopt_list_max_avg_profit', None), - 'filter_min_total_profit': config.get('hyperopt_list_min_total_profit', None), - 'filter_max_total_profit': config.get('hyperopt_list_max_total_profit', None), - 'filter_min_objective': config.get('hyperopt_list_min_objective', None), - 'filter_max_objective': config.get('hyperopt_list_max_objective', None) - } - # Previous evaluations - epochs = HyperoptTools.load_previous_results(results_file) - total_epochs = len(epochs) + epochs, total_epochs = HyperoptTools.load_filtered_results(results_file, config) - epochs = hyperopt_filter_epochs(epochs, filteroptions) filtered_epochs = len(epochs) if n > filtered_epochs: @@ -137,138 +102,3 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None: HyperoptTools.show_epoch_details(val, total_epochs, print_json, no_header, header_str="Epoch details") - - -def hyperopt_filter_epochs(epochs: List, filteroptions: dict) -> List: - """ - Filter our items from the list of hyperopt results - TODO: after 2021.5 remove all "legacy" mode queries. - """ - if filteroptions['only_best']: - epochs = [x for x in epochs if x['is_best']] - if filteroptions['only_profitable']: - epochs = [x for x in epochs if x['results_metrics'].get( - 'profit', x['results_metrics'].get('profit_total', 0)) > 0] - - epochs = _hyperopt_filter_epochs_trade_count(epochs, filteroptions) - - epochs = _hyperopt_filter_epochs_duration(epochs, filteroptions) - - epochs = _hyperopt_filter_epochs_profit(epochs, filteroptions) - - epochs = _hyperopt_filter_epochs_objective(epochs, filteroptions) - - logger.info(f"{len(epochs)} " + - ("best " if filteroptions['only_best'] else "") + - ("profitable " if filteroptions['only_profitable'] else "") + - "epochs found.") - return epochs - - -def _hyperopt_filter_epochs_trade(epochs: List, trade_count: int): - """ - Filter epochs with trade-counts > trades - """ - return [ - x for x in epochs - if x['results_metrics'].get( - 'trade_count', x['results_metrics'].get('total_trades', 0) - ) > trade_count - ] - - -def _hyperopt_filter_epochs_trade_count(epochs: List, filteroptions: dict) -> List: - - if filteroptions['filter_min_trades'] > 0: - epochs = _hyperopt_filter_epochs_trade(epochs, filteroptions['filter_min_trades']) - - if filteroptions['filter_max_trades'] > 0: - epochs = [ - x for x in epochs - if x['results_metrics'].get( - 'trade_count', x['results_metrics'].get('total_trades') - ) < filteroptions['filter_max_trades'] - ] - return epochs - - -def _hyperopt_filter_epochs_duration(epochs: List, filteroptions: dict) -> List: - - def get_duration_value(x): - # Duration in minutes ... - if 'duration' in x['results_metrics']: - return x['results_metrics']['duration'] - else: - # New mode - if 'holding_avg_s' in x['results_metrics']: - avg = x['results_metrics']['holding_avg_s'] - return avg // 60 - raise OperationalException( - "Holding-average not available. Please omit the filter on average time, " - "or rerun hyperopt with this version") - - if filteroptions['filter_min_avg_time'] is not None: - epochs = _hyperopt_filter_epochs_trade(epochs, 0) - epochs = [ - x for x in epochs - if get_duration_value(x) > filteroptions['filter_min_avg_time'] - ] - if filteroptions['filter_max_avg_time'] is not None: - epochs = _hyperopt_filter_epochs_trade(epochs, 0) - epochs = [ - x for x in epochs - if get_duration_value(x) < filteroptions['filter_max_avg_time'] - ] - - return epochs - - -def _hyperopt_filter_epochs_profit(epochs: List, filteroptions: dict) -> List: - - if filteroptions['filter_min_avg_profit'] is not None: - epochs = _hyperopt_filter_epochs_trade(epochs, 0) - epochs = [ - x for x in epochs - if x['results_metrics'].get( - 'avg_profit', x['results_metrics'].get('profit_mean', 0) * 100 - ) > filteroptions['filter_min_avg_profit'] - ] - if filteroptions['filter_max_avg_profit'] is not None: - epochs = _hyperopt_filter_epochs_trade(epochs, 0) - epochs = [ - x for x in epochs - if x['results_metrics'].get( - 'avg_profit', x['results_metrics'].get('profit_mean', 0) * 100 - ) < filteroptions['filter_max_avg_profit'] - ] - if filteroptions['filter_min_total_profit'] is not None: - epochs = _hyperopt_filter_epochs_trade(epochs, 0) - epochs = [ - x for x in epochs - if x['results_metrics'].get( - 'profit', x['results_metrics'].get('profit_total_abs', 0) - ) > filteroptions['filter_min_total_profit'] - ] - if filteroptions['filter_max_total_profit'] is not None: - epochs = _hyperopt_filter_epochs_trade(epochs, 0) - epochs = [ - x for x in epochs - if x['results_metrics'].get( - 'profit', x['results_metrics'].get('profit_total_abs', 0) - ) < filteroptions['filter_max_total_profit'] - ] - return epochs - - -def _hyperopt_filter_epochs_objective(epochs: List, filteroptions: dict) -> List: - - if filteroptions['filter_min_objective'] is not None: - epochs = _hyperopt_filter_epochs_trade(epochs, 0) - - epochs = [x for x in epochs if x['loss'] < filteroptions['filter_min_objective']] - if filteroptions['filter_max_objective'] is not None: - epochs = _hyperopt_filter_epochs_trade(epochs, 0) - - epochs = [x for x in epochs if x['loss'] > filteroptions['filter_max_objective']] - - return epochs diff --git a/freqtrade/optimize/hyperopt_epoch_filters.py b/freqtrade/optimize/hyperopt_epoch_filters.py new file mode 100644 index 000000000..2084c5ba8 --- /dev/null +++ b/freqtrade/optimize/hyperopt_epoch_filters.py @@ -0,0 +1,142 @@ +import logging +from typing import List + +from freqtrade.exceptions import OperationalException + + +logger = logging.getLogger(__name__) + + +def hyperopt_filter_epochs(epochs: List, filteroptions: dict) -> List: + """ + Filter our items from the list of hyperopt results + TODO: after 2021.5 remove all "legacy" mode queries. + """ + if filteroptions['only_best']: + epochs = [x for x in epochs if x['is_best']] + if filteroptions['only_profitable']: + epochs = [x for x in epochs if x['results_metrics'].get( + 'profit', x['results_metrics'].get('profit_total', 0)) > 0] + + epochs = _hyperopt_filter_epochs_trade_count(epochs, filteroptions) + + epochs = _hyperopt_filter_epochs_duration(epochs, filteroptions) + + epochs = _hyperopt_filter_epochs_profit(epochs, filteroptions) + + epochs = _hyperopt_filter_epochs_objective(epochs, filteroptions) + + logger.info(f"{len(epochs)} " + + ("best " if filteroptions['only_best'] else "") + + ("profitable " if filteroptions['only_profitable'] else "") + + "epochs found.") + return epochs + + +def _hyperopt_filter_epochs_trade(epochs: List, trade_count: int): + """ + Filter epochs with trade-counts > trades + """ + return [ + x for x in epochs + if x['results_metrics'].get( + 'trade_count', x['results_metrics'].get('total_trades', 0) + ) > trade_count + ] + + +def _hyperopt_filter_epochs_trade_count(epochs: List, filteroptions: dict) -> List: + + if filteroptions['filter_min_trades'] > 0: + epochs = _hyperopt_filter_epochs_trade(epochs, filteroptions['filter_min_trades']) + + if filteroptions['filter_max_trades'] > 0: + epochs = [ + x for x in epochs + if x['results_metrics'].get( + 'trade_count', x['results_metrics'].get('total_trades') + ) < filteroptions['filter_max_trades'] + ] + return epochs + + +def _hyperopt_filter_epochs_duration(epochs: List, filteroptions: dict) -> List: + + def get_duration_value(x): + # Duration in minutes ... + if 'duration' in x['results_metrics']: + return x['results_metrics']['duration'] + else: + # New mode + if 'holding_avg_s' in x['results_metrics']: + avg = x['results_metrics']['holding_avg_s'] + return avg // 60 + raise OperationalException( + "Holding-average not available. Please omit the filter on average time, " + "or rerun hyperopt with this version") + + if filteroptions['filter_min_avg_time'] is not None: + epochs = _hyperopt_filter_epochs_trade(epochs, 0) + epochs = [ + x for x in epochs + if get_duration_value(x) > filteroptions['filter_min_avg_time'] + ] + if filteroptions['filter_max_avg_time'] is not None: + epochs = _hyperopt_filter_epochs_trade(epochs, 0) + epochs = [ + x for x in epochs + if get_duration_value(x) < filteroptions['filter_max_avg_time'] + ] + + return epochs + + +def _hyperopt_filter_epochs_profit(epochs: List, filteroptions: dict) -> List: + + if filteroptions['filter_min_avg_profit'] is not None: + epochs = _hyperopt_filter_epochs_trade(epochs, 0) + epochs = [ + x for x in epochs + if x['results_metrics'].get( + 'avg_profit', x['results_metrics'].get('profit_mean', 0) * 100 + ) > filteroptions['filter_min_avg_profit'] + ] + if filteroptions['filter_max_avg_profit'] is not None: + epochs = _hyperopt_filter_epochs_trade(epochs, 0) + epochs = [ + x for x in epochs + if x['results_metrics'].get( + 'avg_profit', x['results_metrics'].get('profit_mean', 0) * 100 + ) < filteroptions['filter_max_avg_profit'] + ] + if filteroptions['filter_min_total_profit'] is not None: + epochs = _hyperopt_filter_epochs_trade(epochs, 0) + epochs = [ + x for x in epochs + if x['results_metrics'].get( + 'profit', x['results_metrics'].get('profit_total_abs', 0) + ) > filteroptions['filter_min_total_profit'] + ] + if filteroptions['filter_max_total_profit'] is not None: + epochs = _hyperopt_filter_epochs_trade(epochs, 0) + epochs = [ + x for x in epochs + if x['results_metrics'].get( + 'profit', x['results_metrics'].get('profit_total_abs', 0) + ) < filteroptions['filter_max_total_profit'] + ] + return epochs + + +def _hyperopt_filter_epochs_objective(epochs: List, filteroptions: dict) -> List: + + if filteroptions['filter_min_objective'] is not None: + epochs = _hyperopt_filter_epochs_trade(epochs, 0) + + epochs = [x for x in epochs if x['loss'] < filteroptions['filter_min_objective']] + if filteroptions['filter_max_objective'] is not None: + epochs = _hyperopt_filter_epochs_trade(epochs, 0) + + epochs = [x for x in epochs if x['loss'] > filteroptions['filter_max_objective']] + + return epochs diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index 52aa85c84..ed9583bdc 100755 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -4,7 +4,7 @@ import logging from copy import deepcopy from datetime import datetime, timezone from pathlib import Path -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, Tuple import numpy as np import rapidjson @@ -15,6 +15,7 @@ from pandas import isna, json_normalize from freqtrade.constants import FTHYPT_FILEVERSION, USERPATH_STRATEGIES from freqtrade.exceptions import OperationalException from freqtrade.misc import deep_merge_dicts, round_coin_value, round_dict, safe_value_fallback2 +from freqtrade.optimize.hyperopt_epoch_filters import hyperopt_filter_epochs logger = logging.getLogger(__name__) @@ -130,6 +131,31 @@ class HyperoptTools(): logger.info(f"Loaded {len(epochs)} previous evaluations from disk.") return epochs + @staticmethod + def load_filtered_results(results_file: Path, config: Dict[str, Any]) -> Tuple[List, int]: + filteroptions = { + 'only_best': config.get('hyperopt_list_best', False), + 'only_profitable': config.get('hyperopt_list_profitable', False), + 'filter_min_trades': config.get('hyperopt_list_min_trades', 0), + 'filter_max_trades': config.get('hyperopt_list_max_trades', 0), + 'filter_min_avg_time': config.get('hyperopt_list_min_avg_time', None), + 'filter_max_avg_time': config.get('hyperopt_list_max_avg_time', None), + 'filter_min_avg_profit': config.get('hyperopt_list_min_avg_profit', None), + 'filter_max_avg_profit': config.get('hyperopt_list_max_avg_profit', None), + 'filter_min_total_profit': config.get('hyperopt_list_min_total_profit', None), + 'filter_max_total_profit': config.get('hyperopt_list_max_total_profit', None), + 'filter_min_objective': config.get('hyperopt_list_min_objective', None), + 'filter_max_objective': config.get('hyperopt_list_max_objective', None), + } + + # Previous evaluations + epochs = HyperoptTools.load_previous_results(results_file) + total_epochs = len(epochs) + + epochs = hyperopt_filter_epochs(epochs, filteroptions) + + return epochs, total_epochs + @staticmethod def show_epoch_details(results, total_epochs: int, print_json: bool, no_header: bool = False, header_str: str = None) -> None: From faf16a64e507dc684d527a804abef266e39c26c6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 8 Aug 2021 10:22:45 +0200 Subject: [PATCH 024/100] Remove legacy hyperopt file support --- freqtrade/optimize/hyperopt_epoch_filters.py | 50 +- tests/commands/test_commands.py | 464 +++++++++---------- tests/conftest.py | 132 ------ 3 files changed, 249 insertions(+), 397 deletions(-) diff --git a/freqtrade/optimize/hyperopt_epoch_filters.py b/freqtrade/optimize/hyperopt_epoch_filters.py index 2084c5ba8..b70db94af 100644 --- a/freqtrade/optimize/hyperopt_epoch_filters.py +++ b/freqtrade/optimize/hyperopt_epoch_filters.py @@ -10,13 +10,12 @@ logger = logging.getLogger(__name__) def hyperopt_filter_epochs(epochs: List, filteroptions: dict) -> List: """ Filter our items from the list of hyperopt results - TODO: after 2021.5 remove all "legacy" mode queries. """ if filteroptions['only_best']: epochs = [x for x in epochs if x['is_best']] if filteroptions['only_profitable']: - epochs = [x for x in epochs if x['results_metrics'].get( - 'profit', x['results_metrics'].get('profit_total', 0)) > 0] + epochs = [x for x in epochs + if x['results_metrics'].get('profit_total', 0) > 0] epochs = _hyperopt_filter_epochs_trade_count(epochs, filteroptions) @@ -38,10 +37,7 @@ def _hyperopt_filter_epochs_trade(epochs: List, trade_count: int): Filter epochs with trade-counts > trades """ return [ - x for x in epochs - if x['results_metrics'].get( - 'trade_count', x['results_metrics'].get('total_trades', 0) - ) > trade_count + x for x in epochs if x['results_metrics'].get('total_trades', 0) > trade_count ] @@ -53,9 +49,7 @@ def _hyperopt_filter_epochs_trade_count(epochs: List, filteroptions: dict) -> Li if filteroptions['filter_max_trades'] > 0: epochs = [ x for x in epochs - if x['results_metrics'].get( - 'trade_count', x['results_metrics'].get('total_trades') - ) < filteroptions['filter_max_trades'] + if x['results_metrics'].get('total_trades') < filteroptions['filter_max_trades'] ] return epochs @@ -64,16 +58,12 @@ def _hyperopt_filter_epochs_duration(epochs: List, filteroptions: dict) -> List: def get_duration_value(x): # Duration in minutes ... - if 'duration' in x['results_metrics']: - return x['results_metrics']['duration'] - else: - # New mode - if 'holding_avg_s' in x['results_metrics']: - avg = x['results_metrics']['holding_avg_s'] - return avg // 60 - raise OperationalException( - "Holding-average not available. Please omit the filter on average time, " - "or rerun hyperopt with this version") + if 'holding_avg_s' in x['results_metrics']: + avg = x['results_metrics']['holding_avg_s'] + return avg // 60 + raise OperationalException( + "Holding-average not available. Please omit the filter on average time, " + "or rerun hyperopt with this version") if filteroptions['filter_min_avg_time'] is not None: epochs = _hyperopt_filter_epochs_trade(epochs, 0) @@ -97,33 +87,29 @@ def _hyperopt_filter_epochs_profit(epochs: List, filteroptions: dict) -> List: epochs = _hyperopt_filter_epochs_trade(epochs, 0) epochs = [ x for x in epochs - if x['results_metrics'].get( - 'avg_profit', x['results_metrics'].get('profit_mean', 0) * 100 - ) > filteroptions['filter_min_avg_profit'] + if x['results_metrics'].get('profit_mean', 0) * 100 + > filteroptions['filter_min_avg_profit'] ] if filteroptions['filter_max_avg_profit'] is not None: epochs = _hyperopt_filter_epochs_trade(epochs, 0) epochs = [ x for x in epochs - if x['results_metrics'].get( - 'avg_profit', x['results_metrics'].get('profit_mean', 0) * 100 - ) < filteroptions['filter_max_avg_profit'] + if x['results_metrics'].get('profit_mean', 0) * 100 + < filteroptions['filter_max_avg_profit'] ] if filteroptions['filter_min_total_profit'] is not None: epochs = _hyperopt_filter_epochs_trade(epochs, 0) epochs = [ x for x in epochs - if x['results_metrics'].get( - 'profit', x['results_metrics'].get('profit_total_abs', 0) - ) > filteroptions['filter_min_total_profit'] + if x['results_metrics'].get('profit_total_abs', 0) + > filteroptions['filter_min_total_profit'] ] if filteroptions['filter_max_total_profit'] is not None: epochs = _hyperopt_filter_epochs_trade(epochs, 0) epochs = [ x for x in epochs - if x['results_metrics'].get( - 'profit', x['results_metrics'].get('profit_total_abs', 0) - ) < filteroptions['filter_max_total_profit'] + if x['results_metrics'].get('profit_total_abs', 0) + < filteroptions['filter_max_total_profit'] ] return epochs diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index c0268038a..80dd04b27 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -938,241 +938,239 @@ def test_start_test_pairlist(mocker, caplog, tickers, default_conf, capsys): pytest.fail(f'Expected well formed JSON, but failed to parse: {captured.out}') -def test_hyperopt_list(mocker, capsys, caplog, saved_hyperopt_results, - saved_hyperopt_results_legacy, tmpdir): +def test_hyperopt_list(mocker, capsys, caplog, saved_hyperopt_results, tmpdir): csv_file = Path(tmpdir) / "test.csv" - for res in (saved_hyperopt_results, saved_hyperopt_results_legacy): - mocker.patch( - 'freqtrade.optimize.hyperopt_tools.HyperoptTools.load_previous_results', - MagicMock(return_value=res) - ) + mocker.patch( + 'freqtrade.optimize.hyperopt_tools.HyperoptTools.load_previous_results', + MagicMock(return_value=saved_hyperopt_results) + ) - args = [ - "hyperopt-list", - "--no-details", - "--no-color", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - assert all(x in captured.out - for x in [" 1/12", " 2/12", " 3/12", " 4/12", " 5/12", - " 6/12", " 7/12", " 8/12", " 9/12", " 10/12", - " 11/12", " 12/12"]) - args = [ - "hyperopt-list", - "--best", - "--no-details", - "--no-color", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - assert all(x in captured.out - for x in [" 1/12", " 5/12", " 10/12"]) - assert all(x not in captured.out - for x in [" 2/12", " 3/12", " 4/12", " 6/12", " 7/12", " 8/12", " 9/12", - " 11/12", " 12/12"]) - args = [ - "hyperopt-list", - "--profitable", - "--no-details", - "--no-color", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - assert all(x in captured.out - for x in [" 2/12", " 10/12"]) - assert all(x not in captured.out - for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12", - " 11/12", " 12/12"]) - args = [ - "hyperopt-list", - "--profitable", - "--no-color", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - assert all(x in captured.out - for x in [" 2/12", " 10/12", "Best result:", "Buy hyperspace params", - "Sell hyperspace params", "ROI table", "Stoploss"]) - assert all(x not in captured.out - for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12", - " 11/12", " 12/12"]) - args = [ - "hyperopt-list", - "--no-details", - "--no-color", - "--min-trades", "20", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - assert all(x in captured.out - for x in [" 3/12", " 6/12", " 7/12", " 9/12", " 11/12"]) - assert all(x not in captured.out - for x in [" 1/12", " 2/12", " 4/12", " 5/12", " 8/12", " 10/12", " 12/12"]) - args = [ - "hyperopt-list", - "--profitable", - "--no-details", - "--no-color", - "--max-trades", "20", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - assert all(x in captured.out - for x in [" 2/12", " 10/12"]) - assert all(x not in captured.out - for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12", - " 11/12", " 12/12"]) - args = [ - "hyperopt-list", - "--profitable", - "--no-details", - "--no-color", - "--min-avg-profit", "0.11", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - assert all(x in captured.out - for x in [" 2/12"]) - assert all(x not in captured.out - for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12", - " 10/12", " 11/12", " 12/12"]) - args = [ - "hyperopt-list", - "--no-details", - "--no-color", - "--max-avg-profit", "0.10", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - assert all(x in captured.out - for x in [" 1/12", " 3/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12", - " 11/12"]) - assert all(x not in captured.out - for x in [" 2/12", " 4/12", " 10/12", " 12/12"]) - args = [ - "hyperopt-list", - "--no-details", - "--no-color", - "--min-total-profit", "0.4", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - assert all(x in captured.out - for x in [" 10/12"]) - assert all(x not in captured.out - for x in [" 1/12", " 2/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", - " 9/12", " 11/12", " 12/12"]) - args = [ - "hyperopt-list", - "--no-details", - "--no-color", - "--max-total-profit", "0.4", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - assert all(x in captured.out - for x in [" 1/12", " 2/12", " 3/12", " 5/12", " 6/12", " 7/12", " 8/12", - " 9/12", " 11/12"]) - assert all(x not in captured.out - for x in [" 4/12", " 10/12", " 12/12"]) - args = [ - "hyperopt-list", - "--no-details", - "--no-color", - "--min-objective", "0.1", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - assert all(x in captured.out - for x in [" 10/12"]) - assert all(x not in captured.out - for x in [" 1/12", " 2/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", - " 9/12", " 11/12", " 12/12"]) - args = [ - "hyperopt-list", - "--no-details", - "--max-objective", "0.1", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - assert all(x in captured.out - for x in [" 1/12", " 2/12", " 3/12", " 5/12", " 6/12", " 7/12", " 8/12", - " 9/12", " 11/12"]) - assert all(x not in captured.out - for x in [" 4/12", " 10/12", " 12/12"]) - args = [ - "hyperopt-list", - "--profitable", - "--no-details", - "--no-color", - "--min-avg-time", "2000", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - assert all(x in captured.out - for x in [" 10/12"]) - assert all(x not in captured.out - for x in [" 1/12", " 2/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", - " 8/12", " 9/12", " 11/12", " 12/12"]) - args = [ - "hyperopt-list", - "--no-details", - "--no-color", - "--max-avg-time", "1500", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - assert all(x in captured.out - for x in [" 2/12", " 6/12"]) - assert all(x not in captured.out - for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 7/12", " 8/12" - " 9/12", " 10/12", " 11/12", " 12/12"]) - args = [ - "hyperopt-list", - "--no-details", - "--no-color", - "--export-csv", - str(csv_file), - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - log_has("CSV file created: test_file.csv", caplog) - assert csv_file.is_file() - line = csv_file.read_text() - assert ('Best,1,2,-1.25%,-1.2222,-0.00125625,,-2.51,"3,930.0 m",0.43662' in line - or "Best,1,2,-1.25%,-1.2222,-0.00125625,,-2.51,2 days 17:30:00,0.43662" in line) - csv_file.unlink() + args = [ + "hyperopt-list", + "--no-details", + "--no-color", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 1/12", " 2/12", " 3/12", " 4/12", " 5/12", + " 6/12", " 7/12", " 8/12", " 9/12", " 10/12", + " 11/12", " 12/12"]) + args = [ + "hyperopt-list", + "--best", + "--no-details", + "--no-color", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 1/12", " 5/12", " 10/12"]) + assert all(x not in captured.out + for x in [" 2/12", " 3/12", " 4/12", " 6/12", " 7/12", " 8/12", " 9/12", + " 11/12", " 12/12"]) + args = [ + "hyperopt-list", + "--profitable", + "--no-details", + "--no-color", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 2/12", " 10/12"]) + assert all(x not in captured.out + for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12", + " 11/12", " 12/12"]) + args = [ + "hyperopt-list", + "--profitable", + "--no-color", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 2/12", " 10/12", "Best result:", "Buy hyperspace params", + "Sell hyperspace params", "ROI table", "Stoploss"]) + assert all(x not in captured.out + for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12", + " 11/12", " 12/12"]) + args = [ + "hyperopt-list", + "--no-details", + "--no-color", + "--min-trades", "20", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 3/12", " 6/12", " 7/12", " 9/12", " 11/12"]) + assert all(x not in captured.out + for x in [" 1/12", " 2/12", " 4/12", " 5/12", " 8/12", " 10/12", " 12/12"]) + args = [ + "hyperopt-list", + "--profitable", + "--no-details", + "--no-color", + "--max-trades", "20", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 2/12", " 10/12"]) + assert all(x not in captured.out + for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12", + " 11/12", " 12/12"]) + args = [ + "hyperopt-list", + "--profitable", + "--no-details", + "--no-color", + "--min-avg-profit", "0.11", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 2/12"]) + assert all(x not in captured.out + for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12", + " 10/12", " 11/12", " 12/12"]) + args = [ + "hyperopt-list", + "--no-details", + "--no-color", + "--max-avg-profit", "0.10", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 1/12", " 3/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12", + " 11/12"]) + assert all(x not in captured.out + for x in [" 2/12", " 4/12", " 10/12", " 12/12"]) + args = [ + "hyperopt-list", + "--no-details", + "--no-color", + "--min-total-profit", "0.4", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 10/12"]) + assert all(x not in captured.out + for x in [" 1/12", " 2/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", + " 9/12", " 11/12", " 12/12"]) + args = [ + "hyperopt-list", + "--no-details", + "--no-color", + "--max-total-profit", "0.4", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 1/12", " 2/12", " 3/12", " 5/12", " 6/12", " 7/12", " 8/12", + " 9/12", " 11/12"]) + assert all(x not in captured.out + for x in [" 4/12", " 10/12", " 12/12"]) + args = [ + "hyperopt-list", + "--no-details", + "--no-color", + "--min-objective", "0.1", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 10/12"]) + assert all(x not in captured.out + for x in [" 1/12", " 2/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", + " 9/12", " 11/12", " 12/12"]) + args = [ + "hyperopt-list", + "--no-details", + "--max-objective", "0.1", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 1/12", " 2/12", " 3/12", " 5/12", " 6/12", " 7/12", " 8/12", + " 9/12", " 11/12"]) + assert all(x not in captured.out + for x in [" 4/12", " 10/12", " 12/12"]) + args = [ + "hyperopt-list", + "--profitable", + "--no-details", + "--no-color", + "--min-avg-time", "2000", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 10/12"]) + assert all(x not in captured.out + for x in [" 1/12", " 2/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", + " 8/12", " 9/12", " 11/12", " 12/12"]) + args = [ + "hyperopt-list", + "--no-details", + "--no-color", + "--max-avg-time", "1500", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 2/12", " 6/12"]) + assert all(x not in captured.out + for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 7/12", " 8/12" + " 9/12", " 10/12", " 11/12", " 12/12"]) + args = [ + "hyperopt-list", + "--no-details", + "--no-color", + "--export-csv", + str(csv_file), + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + log_has("CSV file created: test_file.csv", caplog) + assert csv_file.is_file() + line = csv_file.read_text() + assert ('Best,1,2,-1.25%,-1.2222,-0.00125625,,-2.51,"3,930.0 m",0.43662' in line + or "Best,1,2,-1.25%,-1.2222,-0.00125625,,-2.51,2 days 17:30:00,0.43662" in line) + csv_file.unlink() def test_hyperopt_show(mocker, capsys, saved_hyperopt_results): diff --git a/tests/conftest.py b/tests/conftest.py index 1924e1f95..0c9a96e2b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1814,138 +1814,6 @@ def open_trade(): ) -@pytest.fixture -def saved_hyperopt_results_legacy(): - return [ - { - '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 - '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, 'median_profit': -1.2222, '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 - 'total_profit': -0.00125625, - 'current_epoch': 1, - 'is_initial_point': True, - 'is_best': True - }, { - '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}}, - 'results_metrics': {'trade_count': 1, 'avg_profit': 0.12357, 'median_profit': -1.2222, '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, - 'current_epoch': 2, - 'is_initial_point': True, - 'is_best': False - }, { - 'loss': 14.241196856510731, - 'params_dict': {'mfi-value': 25, 'fastd-value': 16, 'adx-value': 29, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 98, 'sell-fastd-value': 72, 'sell-adx-value': 51, 'sell-rsi-value': 82, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 889, 'roi_t2': 533, 'roi_t3': 263, 'roi_p1': 0.04759065393663096, 'roi_p2': 0.1488819964638463, 'roi_p3': 0.4102801822104605, 'stoploss': -0.05394588767607611}, # noqa: E501 - 'params_details': {'buy': {'mfi-value': 25, 'fastd-value': 16, 'adx-value': 29, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 98, 'sell-fastd-value': 72, 'sell-adx-value': 51, 'sell-rsi-value': 82, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.6067528326109377, 263: 0.19647265040047726, 796: 0.04759065393663096, 1685: 0}, 'stoploss': {'stoploss': -0.05394588767607611}}, # noqa: E501 - 'results_metrics': {'trade_count': 621, 'avg_profit': -0.43883302093397747, 'median_profit': -1.2222, 'total_profit': -0.13639474, 'profit': -272.515306, 'duration': 1691.207729468599}, # noqa: E501 - 'results_explanation': ' 621 trades. Avg profit -0.44%. Total profit -0.13639474 BTC (-272.52Σ%). Avg duration 1691.2 min.', # noqa: E501 - 'total_profit': -0.13639474, - 'current_epoch': 3, - 'is_initial_point': True, - 'is_best': False - }, { - 'loss': 100000, - 'params_dict': {'mfi-value': 13, 'fastd-value': 35, 'adx-value': 39, 'rsi-value': 29, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 87, 'sell-fastd-value': 54, 'sell-adx-value': 63, 'sell-rsi-value': 93, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1402, 'roi_t2': 676, 'roi_t3': 215, 'roi_p1': 0.06264755784937427, 'roi_p2': 0.14258587851894644, 'roi_p3': 0.20671291201040828, 'stoploss': -0.11818343570194478}, # noqa: E501 - 'params_details': {'buy': {'mfi-value': 13, 'fastd-value': 35, 'adx-value': 39, 'rsi-value': 29, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 87, 'sell-fastd-value': 54, 'sell-adx-value': 63, 'sell-rsi-value': 93, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.411946348378729, 215: 0.2052334363683207, 891: 0.06264755784937427, 2293: 0}, 'stoploss': {'stoploss': -0.11818343570194478}}, # noqa: E501 - 'results_metrics': {'trade_count': 0, 'avg_profit': None, 'median_profit': None, 'total_profit': 0, 'profit': 0.0, 'duration': None}, # noqa: E501 - 'results_explanation': ' 0 trades. Avg profit nan%. Total profit 0.00000000 BTC ( 0.00Σ%). Avg duration nan min.', # noqa: E501 - 'total_profit': 0, 'current_epoch': 4, 'is_initial_point': True, 'is_best': False - }, { - 'loss': 0.22195522184191518, - 'params_dict': {'mfi-value': 17, 'fastd-value': 21, 'adx-value': 38, 'rsi-value': 33, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 87, 'sell-fastd-value': 82, 'sell-adx-value': 78, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 1269, 'roi_t2': 601, 'roi_t3': 444, 'roi_p1': 0.07280999507931168, 'roi_p2': 0.08946698095898986, 'roi_p3': 0.1454876733325284, 'stoploss': -0.18181041180901014}, # noqa: E501 - 'params_details': {'buy': {'mfi-value': 17, 'fastd-value': 21, 'adx-value': 38, 'rsi-value': 33, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 87, 'sell-fastd-value': 82, 'sell-adx-value': 78, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.3077646493708299, 444: 0.16227697603830155, 1045: 0.07280999507931168, 2314: 0}, 'stoploss': {'stoploss': -0.18181041180901014}}, # noqa: E501 - 'results_metrics': {'trade_count': 14, 'avg_profit': -0.3539515, 'median_profit': -1.2222, 'total_profit': -0.002480140000000001, 'profit': -4.955321, 'duration': 3402.8571428571427}, # noqa: E501 - 'results_explanation': ' 14 trades. Avg profit -0.35%. Total profit -0.00248014 BTC ( -4.96Σ%). Avg duration 3402.9 min.', # noqa: E501 - 'total_profit': -0.002480140000000001, - 'current_epoch': 5, - 'is_initial_point': True, - 'is_best': True - }, { - 'loss': 0.545315889154162, - 'params_dict': {'mfi-value': 22, 'fastd-value': 43, 'adx-value': 46, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'bb_lower', 'sell-mfi-value': 87, 'sell-fastd-value': 65, 'sell-adx-value': 94, 'sell-rsi-value': 63, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 319, 'roi_t2': 556, 'roi_t3': 216, 'roi_p1': 0.06251955472249589, 'roi_p2': 0.11659519602202795, 'roi_p3': 0.0953744132197762, 'stoploss': -0.024551752215582423}, # noqa: E501 - 'params_details': {'buy': {'mfi-value': 22, 'fastd-value': 43, 'adx-value': 46, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'bb_lower'}, 'sell': {'sell-mfi-value': 87, 'sell-fastd-value': 65, 'sell-adx-value': 94, 'sell-rsi-value': 63, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.2744891639643, 216: 0.17911475074452382, 772: 0.06251955472249589, 1091: 0}, 'stoploss': {'stoploss': -0.024551752215582423}}, # noqa: E501 - 'results_metrics': {'trade_count': 39, 'avg_profit': -0.21400679487179478, 'median_profit': -1.2222, 'total_profit': -0.0041773, 'profit': -8.346264999999997, 'duration': 636.9230769230769}, # noqa: E501 - 'results_explanation': ' 39 trades. Avg profit -0.21%. Total profit -0.00417730 BTC ( -8.35Σ%). Avg duration 636.9 min.', # noqa: E501 - 'total_profit': -0.0041773, - 'current_epoch': 6, - 'is_initial_point': True, - 'is_best': False - }, { - '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 - 'results_metrics': {'trade_count': 318, 'avg_profit': -0.39833954716981146, 'median_profit': -1.2222, '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, - 'current_epoch': 7, - 'is_initial_point': True, - 'is_best': False - }, { - 'loss': 20.0, # noqa: E501 - 'params_dict': {'mfi-value': 24, 'fastd-value': 43, 'adx-value': 33, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'sar_reversal', 'sell-mfi-value': 89, 'sell-fastd-value': 74, 'sell-adx-value': 70, 'sell-rsi-value': 70, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': False, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal', 'roi_t1': 1149, 'roi_t2': 375, 'roi_t3': 289, 'roi_p1': 0.05571820757172588, 'roi_p2': 0.0606240398618907, 'roi_p3': 0.1729012220156157, 'stoploss': -0.1588514289110401}, # noqa: E501 - 'params_details': {'buy': {'mfi-value': 24, 'fastd-value': 43, 'adx-value': 33, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'sar_reversal'}, 'sell': {'sell-mfi-value': 89, 'sell-fastd-value': 74, 'sell-adx-value': 70, 'sell-rsi-value': 70, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': False, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal'}, 'roi': {0: 0.2892434694492323, 289: 0.11634224743361658, 664: 0.05571820757172588, 1813: 0}, 'stoploss': {'stoploss': -0.1588514289110401}}, # noqa: E501 - 'results_metrics': {'trade_count': 1, 'avg_profit': 0.0, 'median_profit': 0.0, 'total_profit': 0.0, 'profit': 0.0, 'duration': 5340.0}, # noqa: E501 - 'results_explanation': ' 1 trades. Avg profit 0.00%. Total profit 0.00000000 BTC ( 0.00Σ%). Avg duration 5340.0 min.', # noqa: E501 - 'total_profit': 0.0, - 'current_epoch': 8, - 'is_initial_point': True, - 'is_best': False - }, { - 'loss': 2.4731817780991223, - 'params_dict': {'mfi-value': 22, 'fastd-value': 20, 'adx-value': 29, 'rsi-value': 40, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'sar_reversal', 'sell-mfi-value': 97, 'sell-fastd-value': 65, 'sell-adx-value': 81, 'sell-rsi-value': 64, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1012, 'roi_t2': 584, 'roi_t3': 422, 'roi_p1': 0.036764323603472565, 'roi_p2': 0.10335480573205287, 'roi_p3': 0.10322347377503042, 'stoploss': -0.2780610808108503}, # noqa: E501 - 'params_details': {'buy': {'mfi-value': 22, 'fastd-value': 20, 'adx-value': 29, 'rsi-value': 40, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'sar_reversal'}, 'sell': {'sell-mfi-value': 97, 'sell-fastd-value': 65, 'sell-adx-value': 81, 'sell-rsi-value': 64, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.2433426031105559, 422: 0.14011912933552545, 1006: 0.036764323603472565, 2018: 0}, 'stoploss': {'stoploss': -0.2780610808108503}}, # noqa: E501 - 'results_metrics': {'trade_count': 229, 'avg_profit': -0.38433433624454144, 'median_profit': -1.2222, 'total_profit': -0.044050070000000004, 'profit': -88.01256299999999, 'duration': 6505.676855895196}, # noqa: E501 - 'results_explanation': ' 229 trades. Avg profit -0.38%. Total profit -0.04405007 BTC ( -88.01Σ%). Avg duration 6505.7 min.', # noqa: E501 - 'total_profit': -0.044050070000000004, # noqa: E501 - 'current_epoch': 9, - 'is_initial_point': True, - 'is_best': False - }, { - 'loss': -0.2604606005845212, # noqa: E501 - 'params_dict': {'mfi-value': 23, 'fastd-value': 24, 'adx-value': 22, 'rsi-value': 24, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 97, 'sell-fastd-value': 70, 'sell-adx-value': 64, 'sell-rsi-value': 80, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal', 'roi_t1': 792, 'roi_t2': 464, 'roi_t3': 215, 'roi_p1': 0.04594053535385903, 'roi_p2': 0.09623192684243963, 'roi_p3': 0.04428219070850663, 'stoploss': -0.16992287161634415}, # noqa: E501 - 'params_details': {'buy': {'mfi-value': 23, 'fastd-value': 24, 'adx-value': 22, 'rsi-value': 24, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 97, 'sell-fastd-value': 70, 'sell-adx-value': 64, 'sell-rsi-value': 80, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal'}, 'roi': {0: 0.18645465290480528, 215: 0.14217246219629864, 679: 0.04594053535385903, 1471: 0}, 'stoploss': {'stoploss': -0.16992287161634415}}, # noqa: E501 - 'results_metrics': {'trade_count': 4, 'avg_profit': 0.1080385, 'median_profit': -1.2222, 'total_profit': 0.00021629, 'profit': 0.432154, 'duration': 2850.0}, # noqa: E501 - 'results_explanation': ' 4 trades. Avg profit 0.11%. Total profit 0.00021629 BTC ( 0.43Σ%). Avg duration 2850.0 min.', # noqa: E501 - 'total_profit': 0.00021629, - 'current_epoch': 10, - 'is_initial_point': True, - 'is_best': True - }, { - 'loss': 4.876465945994304, # noqa: E501 - 'params_dict': {'mfi-value': 20, 'fastd-value': 32, 'adx-value': 49, 'rsi-value': 23, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower', 'sell-mfi-value': 75, 'sell-fastd-value': 56, 'sell-adx-value': 61, 'sell-rsi-value': 62, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 579, 'roi_t2': 614, 'roi_t3': 273, 'roi_p1': 0.05307643172744114, 'roi_p2': 0.1352282078262871, 'roi_p3': 0.1913307406325751, 'stoploss': -0.25728526022513887}, # noqa: E501 - 'params_details': {'buy': {'mfi-value': 20, 'fastd-value': 32, 'adx-value': 49, 'rsi-value': 23, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower'}, 'sell': {'sell-mfi-value': 75, 'sell-fastd-value': 56, 'sell-adx-value': 61, 'sell-rsi-value': 62, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.3796353801863034, 273: 0.18830463955372825, 887: 0.05307643172744114, 1466: 0}, 'stoploss': {'stoploss': -0.25728526022513887}}, # noqa: E501 - 'results_metrics': {'trade_count': 117, 'avg_profit': -1.2698609145299145, 'median_profit': -1.2222, 'total_profit': -0.07436117, 'profit': -148.573727, 'duration': 4282.5641025641025}, # noqa: E501 - 'results_explanation': ' 117 trades. Avg profit -1.27%. Total profit -0.07436117 BTC (-148.57Σ%). Avg duration 4282.6 min.', # noqa: E501 - 'total_profit': -0.07436117, - 'current_epoch': 11, - 'is_initial_point': True, - 'is_best': False - }, { - 'loss': 100000, - 'params_dict': {'mfi-value': 10, 'fastd-value': 36, 'adx-value': 31, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'sar_reversal', 'sell-mfi-value': 80, 'sell-fastd-value': 71, 'sell-adx-value': 60, 'sell-rsi-value': 85, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1156, 'roi_t2': 581, 'roi_t3': 408, 'roi_p1': 0.06860454019988212, 'roi_p2': 0.12473718444931989, 'roi_p3': 0.2896360635226823, 'stoploss': -0.30889015124682806}, # noqa: E501 - 'params_details': {'buy': {'mfi-value': 10, 'fastd-value': 36, 'adx-value': 31, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'sar_reversal'}, 'sell': {'sell-mfi-value': 80, 'sell-fastd-value': 71, 'sell-adx-value': 60, 'sell-rsi-value': 85, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.4829777881718843, 408: 0.19334172464920202, 989: 0.06860454019988212, 2145: 0}, 'stoploss': {'stoploss': -0.30889015124682806}}, # noqa: E501 - 'results_metrics': {'trade_count': 0, 'avg_profit': None, 'median_profit': None, 'total_profit': 0, 'profit': 0.0, 'duration': None}, # noqa: E501 - 'results_explanation': ' 0 trades. Avg profit nan%. Total profit 0.00000000 BTC ( 0.00Σ%). Avg duration nan min.', # noqa: E501 - 'total_profit': 0, - 'current_epoch': 12, - 'is_initial_point': True, - 'is_best': False - } - ] - - @pytest.fixture def saved_hyperopt_results(): hyperopt_res = [ From be240566ba1df4eb282b79f17e69adbd02a2e401 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 8 Aug 2021 10:57:20 +0200 Subject: [PATCH 025/100] Fix random test failure --- tests/strategy/test_interface.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 0ad6d6f32..dc51f0811 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -552,6 +552,7 @@ def test__analyze_ticker_internal_skip_analyze(ohlcv_history, mocker, caplog) -> def test_is_pair_locked(default_conf): default_conf.update({'strategy': 'DefaultStrategy'}) PairLocks.timeframe = default_conf['timeframe'] + PairLocks.use_db = True strategy = StrategyResolver.load_strategy(default_conf) # No lock should be present assert len(PairLocks.get_pair_locks(None)) == 0 From 3bd0c3d0098bbcab9a9b8444e785ff43fbfc9fe7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 8 Aug 2021 11:02:54 +0200 Subject: [PATCH 026/100] Remove legacy code from export to csv --- freqtrade/optimize/hyperopt_tools.py | 28 ++++++++-------------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index ed9583bdc..b51db4db2 100755 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -459,21 +459,14 @@ class HyperoptTools(): trials['Best'] = '' trials['Stake currency'] = config['stake_currency'] - if 'results_metrics.total_trades' in trials: - base_metrics = ['Best', 'current_epoch', 'results_metrics.total_trades', - 'results_metrics.profit_mean', 'results_metrics.profit_median', - 'results_metrics.profit_total', - 'Stake currency', - 'results_metrics.profit_total_abs', 'results_metrics.holding_avg', - 'loss', 'is_initial_point', 'is_best'] - perc_multi = 100 - else: - perc_multi = 1 - base_metrics = ['Best', 'current_epoch', 'results_metrics.trade_count', - 'results_metrics.avg_profit', 'results_metrics.median_profit', - 'results_metrics.total_profit', - 'Stake currency', 'results_metrics.profit', 'results_metrics.duration', - 'loss', 'is_initial_point', 'is_best'] + base_metrics = ['Best', 'current_epoch', 'results_metrics.total_trades', + 'results_metrics.profit_mean', 'results_metrics.profit_median', + 'results_metrics.profit_total', + 'Stake currency', + 'results_metrics.profit_total_abs', 'results_metrics.holding_avg', + 'loss', 'is_initial_point', 'is_best'] + perc_multi = 100 + param_metrics = [("params_dict."+param) for param in results[0]['params_dict'].keys()] trials = trials[base_metrics + param_metrics] @@ -501,11 +494,6 @@ class HyperoptTools(): trials['Avg profit'] = trials['Avg profit'].apply( lambda x: f'{x * perc_multi:,.2f}%' if not isna(x) else "" ) - if perc_multi == 1: - trials['Avg duration'] = trials['Avg duration'].apply( - lambda x: f'{x:,.1f} m' if isinstance( - x, float) else f"{x.total_seconds() // 60:,.1f} m" if not isna(x) else "" - ) trials['Objective'] = trials['Objective'].apply( lambda x: f'{x:,.5f}' if x != 100000 else "" ) From 32e8e3b24258e1a7d12bde1d107d222ef1ee8fd9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Aug 2021 03:01:19 +0000 Subject: [PATCH 027/100] Bump types-cachetools from 0.1.9 to 0.1.10 Bumps [types-cachetools](https://github.com/python/typeshed) from 0.1.9 to 0.1.10. - [Release notes](https://github.com/python/typeshed/releases) - [Commits](https://github.com/python/typeshed/commits) --- updated-dependencies: - dependency-name: types-cachetools dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index c1f7d6486..7b7727bd5 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -19,7 +19,7 @@ isort==5.9.3 nbconvert==6.1.0 # mypy types -types-cachetools==0.1.9 +types-cachetools==0.1.10 types-filelock==0.1.4 types-requests==2.25.1 types-tabulate==0.1.1 From bad25b753c679218557fce23488b13bfe20bc609 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Aug 2021 03:01:25 +0000 Subject: [PATCH 028/100] Bump ccxt from 1.54.24 to 1.54.62 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.54.24 to 1.54.62. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/exchanges.cfg) - [Commits](https://github.com/ccxt/ccxt/compare/1.54.24...1.54.62) --- updated-dependencies: - dependency-name: ccxt dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 6c2ef56c3..60175c12f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.21.1 pandas==1.3.1 -ccxt==1.54.24 +ccxt==1.54.62 # Pin cryptography for now due to rust build errors with piwheels cryptography==3.4.7 aiohttp==3.7.4.post0 From 59626b4ffcaa7f348d8e6986b274d32a65b54009 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Aug 2021 03:01:27 +0000 Subject: [PATCH 029/100] Bump types-tabulate from 0.1.1 to 0.8.2 Bumps [types-tabulate](https://github.com/python/typeshed) from 0.1.1 to 0.8.2. - [Release notes](https://github.com/python/typeshed/releases) - [Commits](https://github.com/python/typeshed/commits) --- updated-dependencies: - dependency-name: types-tabulate dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index c1f7d6486..cbb59be4e 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -22,4 +22,4 @@ nbconvert==6.1.0 types-cachetools==0.1.9 types-filelock==0.1.4 types-requests==2.25.1 -types-tabulate==0.1.1 +types-tabulate==0.8.2 From b89a993890224ead95a8652bc0a83dde89c87838 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Aug 2021 04:47:41 +0000 Subject: [PATCH 030/100] Bump types-filelock from 0.1.4 to 0.1.5 Bumps [types-filelock](https://github.com/python/typeshed) from 0.1.4 to 0.1.5. - [Release notes](https://github.com/python/typeshed/releases) - [Commits](https://github.com/python/typeshed/commits) --- updated-dependencies: - dependency-name: types-filelock dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 8f0cc0b34..7128721c5 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -20,6 +20,6 @@ nbconvert==6.1.0 # mypy types types-cachetools==0.1.10 -types-filelock==0.1.4 +types-filelock==0.1.5 types-requests==2.25.1 types-tabulate==0.8.2 From 47f641d12f9d7813f80369f2691a1633168331a1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 9 Aug 2021 07:03:13 +0200 Subject: [PATCH 031/100] Remove hyperopt-pickle result support --- freqtrade/optimize/hyperopt_tools.py | 17 ++++------------- tests/optimize/test_hyperopt_tools.py | 27 +++------------------------ 2 files changed, 7 insertions(+), 37 deletions(-) diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index b51db4db2..0bb6aba15 100755 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -89,18 +89,6 @@ class HyperoptTools(): else: return any(s in config['spaces'] for s in [space, 'all', 'default']) - @staticmethod - def _read_results_pickle(results_file: Path) -> List: - """ - Read hyperopt results from pickle file - LEGACY method - new files are written as json and cannot be read with this method. - """ - from joblib import load - - logger.info(f"Reading pickled epochs from '{results_file}'") - data = load(results_file) - return data - @staticmethod def _read_results(results_file: Path) -> List: """ @@ -120,7 +108,10 @@ class HyperoptTools(): epochs: List = [] if results_file.is_file() and results_file.stat().st_size > 0: if results_file.suffix == '.pickle': - epochs = HyperoptTools._read_results_pickle(results_file) + raise OperationalException( + "Legacy hyperopt results are no longer supported." + "Please rerun hyperopt or use an older version to load this file." + ) else: epochs = HyperoptTools._read_results(results_file) # Detection of some old format, without 'is_best' field saved diff --git a/tests/optimize/test_hyperopt_tools.py b/tests/optimize/test_hyperopt_tools.py index 44b4a7a03..d59a44da7 100644 --- a/tests/optimize/test_hyperopt_tools.py +++ b/tests/optimize/test_hyperopt_tools.py @@ -10,7 +10,7 @@ import rapidjson from freqtrade.constants import FTHYPT_FILEVERSION from freqtrade.exceptions import OperationalException from freqtrade.optimize.hyperopt_tools import HyperoptTools, hyperopt_serializer -from tests.conftest import log_has, log_has_re +from tests.conftest import log_has # Functions for recurrent object patching @@ -37,31 +37,10 @@ def test_save_results_saves_epochs(hyperopt, tmpdir, caplog) -> None: assert len(hyperopt_epochs) == 2 -def test_load_previous_results(testdatadir, caplog) -> None: - - results_file = testdatadir / 'hyperopt_results_SampleStrategy.pickle' - - hyperopt_epochs = HyperoptTools.load_previous_results(results_file) - - assert len(hyperopt_epochs) == 5 - assert log_has_re(r"Reading pickled epochs from .*", caplog) - - caplog.clear() - - # Modern version - results_file = testdatadir / 'strategy_SampleStrategy.fthypt' - - hyperopt_epochs = HyperoptTools.load_previous_results(results_file) - - assert len(hyperopt_epochs) == 5 - assert log_has_re(r"Reading epochs from .*", caplog) - - def test_load_previous_results2(mocker, testdatadir, caplog) -> None: - mocker.patch('freqtrade.optimize.hyperopt_tools.HyperoptTools._read_results_pickle', - return_value=[{'asdf': '222'}]) results_file = testdatadir / 'hyperopt_results_SampleStrategy.pickle' - with pytest.raises(OperationalException, match=r"The file .* incompatible.*"): + with pytest.raises(OperationalException, + match=r"Legacy hyperopt results are no longer supported.*"): HyperoptTools.load_previous_results(results_file) From 5919992ad2ab67ad3aaffd92bdaa6ccec30fd537 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Aug 2021 05:24:31 +0000 Subject: [PATCH 032/100] Bump types-requests from 2.25.1 to 2.25.6 Bumps [types-requests](https://github.com/python/typeshed) from 2.25.1 to 2.25.6. - [Release notes](https://github.com/python/typeshed/releases) - [Commits](https://github.com/python/typeshed/commits) --- updated-dependencies: - dependency-name: types-requests dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 7128721c5..9629bbea1 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -21,5 +21,5 @@ nbconvert==6.1.0 # mypy types types-cachetools==0.1.10 types-filelock==0.1.5 -types-requests==2.25.1 +types-requests==2.25.6 types-tabulate==0.8.2 From f17942b68fa23a5dc727cba463e92f0a71e8cfb3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 9 Aug 2021 11:18:18 +0200 Subject: [PATCH 033/100] Fix random test failure --- freqtrade/optimize/backtesting.py | 3 +++ tests/optimize/test_backtesting.py | 2 ++ 2 files changed, 5 insertions(+) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 3079e326d..85137a8ef 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -130,6 +130,9 @@ class Backtesting: self.abort = False def __del__(self): + self.cleanup() + + def cleanup(self): LoggingMixin.show_output = True PairLocks.use_db = True Trade.use_db = True diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index deaaf9f2f..b859e9017 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -535,6 +535,8 @@ def test_backtest__enter_trade(default_conf, fee, mocker) -> None: trade = backtesting._enter_trade(pair, row=row) assert trade is None + backtesting.cleanup() + def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None: default_conf['use_sell_signal'] = False From 927ac24f82f66c3932936ea451f39efde193adfd Mon Sep 17 00:00:00 2001 From: pcassimans Date: Mon, 9 Aug 2021 14:21:59 +0200 Subject: [PATCH 034/100] Update README.md Fix Typo of Kukoin to Kucoin --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 78ea3cecd..309fab94b 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ Please read the [exchange specific notes](docs/exchanges.md) to learn about even Exchanges confirmed working by the community: - [X] [Bitvavo](https://bitvavo.com/) -- [X] [Kukoin](https://www.kucoin.com/) +- [X] [Kucoin](https://www.kucoin.com/) ## Documentation From 519c256b88e31f8347bd5bf7395a08d0c774fdae Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 9 Aug 2021 14:43:35 +0200 Subject: [PATCH 035/100] Fix kucoin typo in index.md as well --- docs/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index 05eaa7552..fd3b8f224 100644 --- a/docs/index.md +++ b/docs/index.md @@ -47,7 +47,7 @@ Please read the [exchange specific notes](exchanges.md) to learn about eventual, Exchanges confirmed working by the community: - [X] [Bitvavo](https://bitvavo.com/) -- [X] [Kukoin](https://www.kucoin.com/) +- [X] [Kucoin](https://www.kucoin.com/) ## Requirements From a5f796bc9764040be80478deb246fe08b814be9f Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 9 Aug 2021 14:53:18 +0200 Subject: [PATCH 036/100] refactor ohlcvdata_to_dataframe to advise_all_indicators --- freqtrade/edge/edge_positioning.py | 2 +- freqtrade/optimize/backtesting.py | 5 +++-- freqtrade/optimize/hyperopt.py | 2 +- freqtrade/strategy/interface.py | 2 +- tests/data/test_history.py | 6 +++--- tests/optimize/test_backtesting.py | 18 +++++++++--------- tests/optimize/test_hyperopt.py | 24 ++++++++++++------------ tests/strategy/test_interface.py | 8 ++++---- 8 files changed, 34 insertions(+), 33 deletions(-) diff --git a/freqtrade/edge/edge_positioning.py b/freqtrade/edge/edge_positioning.py index 243043d31..f12b1b37d 100644 --- a/freqtrade/edge/edge_positioning.py +++ b/freqtrade/edge/edge_positioning.py @@ -151,7 +151,7 @@ class Edge: # Fake run-mode to Edge prior_rm = self.config['runmode'] self.config['runmode'] = RunMode.EDGE - preprocessed = self.strategy.ohlcvdata_to_dataframe(data) + preprocessed = self.strategy.advise_all_indicators(data) self.config['runmode'] = prior_rm # Print timeframe diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 85137a8ef..825d1dd25 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -533,7 +533,8 @@ class Backtesting: 'final_balance': self.wallets.get_total(self.strategy.config['stake_currency']), } - def backtest_one_strategy(self, strat: IStrategy, data: Dict[str, Any], timerange: TimeRange): + def backtest_one_strategy(self, strat: IStrategy, data: Dict[str, DataFrame], + timerange: TimeRange): self.progress.init_step(BacktestState.ANALYZE, 0) logger.info("Running backtesting for Strategy %s", strat.get_strategy_name()) @@ -552,7 +553,7 @@ class Backtesting: max_open_trades = 0 # need to reprocess data every time to populate signals - preprocessed = self.strategy.ohlcvdata_to_dataframe(data) + preprocessed = self.strategy.advise_all_indicators(data) # Trim startup period from analyzed dataframe preprocessed_tmp = trim_dataframes(preprocessed, timerange, self.required_startup) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 0db78aa39..901900121 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -394,7 +394,7 @@ class Hyperopt: data, timerange = self.backtesting.load_bt_data() logger.info("Dataload complete. Calculating indicators") - preprocessed = self.backtesting.strategy.ohlcvdata_to_dataframe(data) + preprocessed = self.backtesting.strategy.advise_all_indicators(data) # Trim startup period from analyzed dataframe to get correct dates for output. processed = trim_dataframes(preprocessed, timerange, self.backtesting.required_startup) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index bf5cc10af..d4f10301a 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -732,7 +732,7 @@ class IStrategy(ABC, HyperStrategyMixin): else: return current_profit > roi - def ohlcvdata_to_dataframe(self, data: Dict[str, DataFrame]) -> Dict[str, DataFrame]: + def advise_all_indicators(self, data: Dict[str, DataFrame]) -> Dict[str, DataFrame]: """ Populates indicators for given candle (OHLCV) data (for multiple pairs) Does not run advise_buy or advise_sell! diff --git a/tests/data/test_history.py b/tests/data/test_history.py index d203d0792..9cfe861ea 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -381,7 +381,7 @@ def test_get_timerange(default_conf, mocker, testdatadir) -> None: default_conf.update({'strategy': 'DefaultStrategy'}) strategy = StrategyResolver.load_strategy(default_conf) - data = strategy.ohlcvdata_to_dataframe( + data = strategy.advise_all_indicators( load_data( datadir=testdatadir, timeframe='1m', @@ -399,7 +399,7 @@ def test_validate_backtest_data_warn(default_conf, mocker, caplog, testdatadir) default_conf.update({'strategy': 'DefaultStrategy'}) strategy = StrategyResolver.load_strategy(default_conf) - data = strategy.ohlcvdata_to_dataframe( + data = strategy.advise_all_indicators( load_data( datadir=testdatadir, timeframe='1m', @@ -424,7 +424,7 @@ def test_validate_backtest_data(default_conf, mocker, caplog, testdatadir) -> No strategy = StrategyResolver.load_strategy(default_conf) timerange = TimeRange('index', 'index', 200, 250) - data = strategy.ohlcvdata_to_dataframe( + data = strategy.advise_all_indicators( load_data( datadir=testdatadir, timeframe='5m', diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index b859e9017..8e3d4063a 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -85,7 +85,7 @@ def simple_backtest(config, contour, mocker, testdatadir) -> None: backtesting._set_strategy(backtesting.strategylist[0]) data = load_data_test(contour, testdatadir) - processed = backtesting.strategy.ohlcvdata_to_dataframe(data) + processed = backtesting.strategy.advise_all_indicators(data) min_date, max_date = get_timerange(processed) assert isinstance(processed, dict) results = backtesting.backtest( @@ -107,7 +107,7 @@ def _make_backtest_conf(mocker, datadir, conf=None, pair='UNITTEST/BTC'): patch_exchange(mocker) backtesting = Backtesting(conf) backtesting._set_strategy(backtesting.strategylist[0]) - processed = backtesting.strategy.ohlcvdata_to_dataframe(data) + processed = backtesting.strategy.advise_all_indicators(data) min_date, max_date = get_timerange(processed) return { 'processed': processed, @@ -289,7 +289,7 @@ def test_backtesting_init(mocker, default_conf, order_types) -> None: backtesting._set_strategy(backtesting.strategylist[0]) assert backtesting.config == default_conf assert backtesting.timeframe == '5m' - assert callable(backtesting.strategy.ohlcvdata_to_dataframe) + assert callable(backtesting.strategy.advise_all_indicators) assert callable(backtesting.strategy.advise_buy) assert callable(backtesting.strategy.advise_sell) assert isinstance(backtesting.strategy.dp, DataProvider) @@ -335,14 +335,14 @@ def test_data_to_dataframe_bt(default_conf, mocker, testdatadir) -> None: fill_up_missing=True) backtesting = Backtesting(default_conf) backtesting._set_strategy(backtesting.strategylist[0]) - processed = backtesting.strategy.ohlcvdata_to_dataframe(data) + processed = backtesting.strategy.advise_all_indicators(data) assert len(processed['UNITTEST/BTC']) == 102 # Load strategy to compare the result between Backtesting function and strategy are the same default_conf.update({'strategy': 'DefaultStrategy'}) strategy = StrategyResolver.load_strategy(default_conf) - processed2 = strategy.ohlcvdata_to_dataframe(data) + processed2 = strategy.advise_all_indicators(data) assert processed['UNITTEST/BTC'].equals(processed2['UNITTEST/BTC']) @@ -549,7 +549,7 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None: timerange = TimeRange('date', None, 1517227800, 0) data = history.load_data(datadir=testdatadir, timeframe='5m', pairs=['UNITTEST/BTC'], timerange=timerange) - processed = backtesting.strategy.ohlcvdata_to_dataframe(data) + processed = backtesting.strategy.advise_all_indicators(data) min_date, max_date = get_timerange(processed) result = backtesting.backtest( processed=processed, @@ -614,7 +614,7 @@ def test_backtest_1min_timeframe(default_conf, fee, mocker, testdatadir) -> None timerange = TimeRange.parse_timerange('1510688220-1510700340') data = history.load_data(datadir=testdatadir, timeframe='1m', pairs=['UNITTEST/BTC'], timerange=timerange) - processed = backtesting.strategy.ohlcvdata_to_dataframe(data) + processed = backtesting.strategy.advise_all_indicators(data) min_date, max_date = get_timerange(processed) results = backtesting.backtest( processed=processed, @@ -633,7 +633,7 @@ def test_processed(default_conf, mocker, testdatadir) -> None: backtesting._set_strategy(backtesting.strategylist[0]) dict_of_tickerrows = load_data_test('raise', testdatadir) - dataframes = backtesting.strategy.ohlcvdata_to_dataframe(dict_of_tickerrows) + dataframes = backtesting.strategy.advise_all_indicators(dict_of_tickerrows) dataframe = dataframes['UNITTEST/BTC'] cols = dataframe.columns # assert the dataframe got some of the indicator columns @@ -782,7 +782,7 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir) backtesting.strategy.advise_buy = _trend_alternate_hold # Override backtesting.strategy.advise_sell = _trend_alternate_hold # Override - processed = backtesting.strategy.ohlcvdata_to_dataframe(data) + processed = backtesting.strategy.advise_all_indicators(data) min_date, max_date = get_timerange(processed) backtest_conf = { 'processed': processed, diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index d146e84f1..0ca79d268 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -351,7 +351,7 @@ def test_start_calls_optimizer(mocker, hyperopt_conf, capsys) -> None: del hyperopt_conf['timeframe'] hyperopt = Hyperopt(hyperopt_conf) - hyperopt.backtesting.strategy.ohlcvdata_to_dataframe = MagicMock() + hyperopt.backtesting.strategy.advise_all_indicators = MagicMock() hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) hyperopt.start() @@ -426,7 +426,7 @@ def test_hyperopt_format_results(hyperopt): def test_populate_indicators(hyperopt, testdatadir) -> None: data = load_data(testdatadir, '1m', ['UNITTEST/BTC'], fill_up_missing=True) - dataframes = hyperopt.backtesting.strategy.ohlcvdata_to_dataframe(data) + dataframes = hyperopt.backtesting.strategy.advise_all_indicators(data) dataframe = hyperopt.custom_hyperopt.populate_indicators(dataframes['UNITTEST/BTC'], {'pair': 'UNITTEST/BTC'}) @@ -438,7 +438,7 @@ def test_populate_indicators(hyperopt, testdatadir) -> None: def test_buy_strategy_generator(hyperopt, testdatadir) -> None: data = load_data(testdatadir, '1m', ['UNITTEST/BTC'], fill_up_missing=True) - dataframes = hyperopt.backtesting.strategy.ohlcvdata_to_dataframe(data) + dataframes = hyperopt.backtesting.strategy.advise_all_indicators(data) dataframe = hyperopt.custom_hyperopt.populate_indicators(dataframes['UNITTEST/BTC'], {'pair': 'UNITTEST/BTC'}) @@ -463,7 +463,7 @@ def test_buy_strategy_generator(hyperopt, testdatadir) -> None: def test_sell_strategy_generator(hyperopt, testdatadir) -> None: data = load_data(testdatadir, '1m', ['UNITTEST/BTC'], fill_up_missing=True) - dataframes = hyperopt.backtesting.strategy.ohlcvdata_to_dataframe(data) + dataframes = hyperopt.backtesting.strategy.advise_all_indicators(data) dataframe = hyperopt.custom_hyperopt.populate_indicators(dataframes['UNITTEST/BTC'], {'pair': 'UNITTEST/BTC'}) @@ -660,7 +660,7 @@ def test_print_json_spaces_all(mocker, hyperopt_conf, capsys) -> None: }) hyperopt = Hyperopt(hyperopt_conf) - hyperopt.backtesting.strategy.ohlcvdata_to_dataframe = MagicMock() + hyperopt.backtesting.strategy.advise_all_indicators = MagicMock() hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) hyperopt.start() @@ -713,7 +713,7 @@ def test_print_json_spaces_default(mocker, hyperopt_conf, capsys) -> None: hyperopt_conf.update({'print_json': True}) hyperopt = Hyperopt(hyperopt_conf) - hyperopt.backtesting.strategy.ohlcvdata_to_dataframe = MagicMock() + hyperopt.backtesting.strategy.advise_all_indicators = MagicMock() hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) hyperopt.start() @@ -761,7 +761,7 @@ def test_print_json_spaces_roi_stoploss(mocker, hyperopt_conf, capsys) -> None: }) hyperopt = Hyperopt(hyperopt_conf) - hyperopt.backtesting.strategy.ohlcvdata_to_dataframe = MagicMock() + hyperopt.backtesting.strategy.advise_all_indicators = MagicMock() hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) hyperopt.start() @@ -805,7 +805,7 @@ def test_simplified_interface_roi_stoploss(mocker, hyperopt_conf, capsys) -> Non hyperopt_conf.update({'spaces': 'roi stoploss'}) hyperopt = Hyperopt(hyperopt_conf) - hyperopt.backtesting.strategy.ohlcvdata_to_dataframe = MagicMock() + hyperopt.backtesting.strategy.advise_all_indicators = MagicMock() hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) del hyperopt.custom_hyperopt.__class__.buy_strategy_generator @@ -844,7 +844,7 @@ def test_simplified_interface_all_failed(mocker, hyperopt_conf) -> None: hyperopt_conf.update({'spaces': 'all', }) hyperopt = Hyperopt(hyperopt_conf) - hyperopt.backtesting.strategy.ohlcvdata_to_dataframe = MagicMock() + hyperopt.backtesting.strategy.advise_all_indicators = MagicMock() hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) del hyperopt.custom_hyperopt.__class__.buy_strategy_generator @@ -886,7 +886,7 @@ def test_simplified_interface_buy(mocker, hyperopt_conf, capsys) -> None: hyperopt_conf.update({'spaces': 'buy'}) hyperopt = Hyperopt(hyperopt_conf) - hyperopt.backtesting.strategy.ohlcvdata_to_dataframe = MagicMock() + hyperopt.backtesting.strategy.advise_all_indicators = MagicMock() hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) # TODO: sell_strategy_generator() is actually not called because @@ -940,7 +940,7 @@ def test_simplified_interface_sell(mocker, hyperopt_conf, capsys) -> None: hyperopt_conf.update({'spaces': 'sell', }) hyperopt = Hyperopt(hyperopt_conf) - hyperopt.backtesting.strategy.ohlcvdata_to_dataframe = MagicMock() + hyperopt.backtesting.strategy.advise_all_indicators = MagicMock() hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) # TODO: buy_strategy_generator() is actually not called because @@ -985,7 +985,7 @@ def test_simplified_interface_failed(mocker, hyperopt_conf, method, space) -> No hyperopt_conf.update({'spaces': space}) hyperopt = Hyperopt(hyperopt_conf) - hyperopt.backtesting.strategy.ohlcvdata_to_dataframe = MagicMock() + hyperopt.backtesting.strategy.advise_all_indicators = MagicMock() hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) delattr(hyperopt.custom_hyperopt.__class__, method) diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index dc51f0811..686a36c96 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -228,25 +228,25 @@ def test_assert_df(ohlcv_history, caplog): _STRATEGY.disable_dataframe_checks = False -def test_ohlcvdata_to_dataframe(default_conf, testdatadir) -> None: +def test_advise_all_indicators(default_conf, testdatadir) -> None: default_conf.update({'strategy': 'DefaultStrategy'}) strategy = StrategyResolver.load_strategy(default_conf) timerange = TimeRange.parse_timerange('1510694220-1510700340') data = load_data(testdatadir, '1m', ['UNITTEST/BTC'], timerange=timerange, fill_up_missing=True) - processed = strategy.ohlcvdata_to_dataframe(data) + processed = strategy.advise_all_indicators(data) assert len(processed['UNITTEST/BTC']) == 102 # partial candle was removed -def test_ohlcvdata_to_dataframe_copy(mocker, default_conf, testdatadir) -> None: +def test_advise_all_indicators_copy(mocker, default_conf, testdatadir) -> None: default_conf.update({'strategy': 'DefaultStrategy'}) strategy = StrategyResolver.load_strategy(default_conf) aimock = mocker.patch('freqtrade.strategy.interface.IStrategy.advise_indicators') timerange = TimeRange.parse_timerange('1510694220-1510700340') data = load_data(testdatadir, '1m', ['UNITTEST/BTC'], timerange=timerange, fill_up_missing=True) - strategy.ohlcvdata_to_dataframe(data) + strategy.advise_all_indicators(data) assert aimock.call_count == 1 # Ensure that a copy of the dataframe is passed to advice_indicators assert aimock.call_args_list[0][0][0] is not data From 895b912c719da2f613c5ae5da47cfb375bf771b8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 9 Aug 2021 14:40:02 +0200 Subject: [PATCH 037/100] Fix recently introduced lookahead bias in backtesting closes #5388 --- freqtrade/optimize/backtesting.py | 4 +++- tests/optimize/test_backtesting.py | 18 ++++++++++++++---- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 825d1dd25..3a864173f 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -466,6 +466,8 @@ class Backtesting: for i, pair in enumerate(data): row_index = indexes[pair] try: + # Row is treated as "current incomplete candle". + # Buy / sell signals are shifted by 1 to compensate for this. row = data[pair][row_index] except IndexError: # missing Data for one pair at the end. @@ -476,8 +478,8 @@ class Backtesting: if row[DATE_IDX] > tmp: continue - row_index += 1 self.dataprovider._set_dataframe_max_index(row_index) + row_index += 1 indexes[pair] = row_index # without positionstacking, we can only have one open trade per pair. diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 8e3d4063a..ff9b81c30 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -1,6 +1,7 @@ # pragma pylint: disable=missing-docstring, W0212, line-too-long, C0103, unused-argument import random +from datetime import timedelta from pathlib import Path from unittest.mock import MagicMock, PropertyMock @@ -741,8 +742,13 @@ def test_backtest_alternate_buy_sell(default_conf, fee, mocker, testdatadir): # 100 buys signals results = result['results'] assert len(results) == 100 - # Cached data should be 200 (no change since required_startup is 0) - assert len(backtesting.dataprovider.get_analyzed_dataframe('UNITTEST/BTC', '1m')[0]) == 200 + # Cached data should be 199 (missing 1 candle at the start) + analyzed_df = backtesting.dataprovider.get_analyzed_dataframe('UNITTEST/BTC', '1m')[0] + assert len(analyzed_df) == 199 + # Expect last candle to be 1 below end date (as the last candle is assumed as "incomplete" + # during backtesting) + expected_last_candle_date = backtest_conf['end_date'] - timedelta(minutes=1) + assert analyzed_df.iloc[-1]['date'].to_pydatetime() == expected_last_candle_date # One trade was force-closed at the end assert len(results.loc[results['is_open']]) == 0 @@ -774,7 +780,8 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir) data = trim_dictlist(data, -500) # Remove data for one pair from the beginning of the data - data[pair] = data[pair][tres:].reset_index() + if tres > 0: + data[pair] = data[pair][tres:].reset_index() default_conf['timeframe'] = '5m' backtesting = Backtesting(default_conf) @@ -800,8 +807,11 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir) assert len(evaluate_result_multi(results['results'], '5m', 3)) == 0 # Cached data correctly removed amounts - removed_candles = len(data[pair]) - 1 - backtesting.strategy.startup_candle_count + offset = 2 if tres == 0 else 1 + removed_candles = len(data[pair]) - offset - backtesting.strategy.startup_candle_count assert len(backtesting.dataprovider.get_analyzed_dataframe(pair, '5m')[0]) == removed_candles + assert len(backtesting.dataprovider.get_analyzed_dataframe( + 'NXT/BTC', '5m')[0]) == len(data['NXT/BTC']) - 2 - backtesting.strategy.startup_candle_count backtest_conf = { 'processed': processed, From 5bfb9edf02922c38aef0f501634eb116c74f7515 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 9 Aug 2021 15:42:17 +0200 Subject: [PATCH 038/100] Only query date once from list --- freqtrade/optimize/backtesting.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 3a864173f..5efb5101f 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -323,14 +323,14 @@ class Backtesting: return sell_row[OPEN_IDX] def _get_sell_trade_entry(self, trade: LocalTrade, sell_row: Tuple) -> Optional[LocalTrade]: - + sell_candle_time = sell_row[DATE_IDX].to_pydatetime() sell = self.strategy.should_sell(trade, sell_row[OPEN_IDX], # type: ignore - sell_row[DATE_IDX].to_pydatetime(), sell_row[BUY_IDX], + sell_candle_time, sell_row[BUY_IDX], sell_row[SELL_IDX], low=sell_row[LOW_IDX], high=sell_row[HIGH_IDX]) if sell.sell_flag: - trade.close_date = sell_row[DATE_IDX].to_pydatetime() + trade.close_date = sell_candle_time trade.sell_reason = sell.sell_reason trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60) closerate = self._get_close_rate(sell_row, trade, sell, trade_dur) @@ -342,7 +342,7 @@ class Backtesting: rate=closerate, time_in_force=time_in_force, sell_reason=sell.sell_reason, - current_time=sell_row[DATE_IDX].to_pydatetime()): + current_time=sell_candle_time): return None trade.close(closerate, show_msg=False) From cf27968b97cc1491b61923a0a95f2183aacf0592 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 9 Aug 2021 19:38:56 +0200 Subject: [PATCH 039/100] Properly preserve trade's low during backtesting --- freqtrade/persistence/models.py | 4 ++-- freqtrade/strategy/interface.py | 2 +- tests/optimize/test_backtesting.py | 2 +- tests/strategy/test_interface.py | 2 +- tests/test_persistence.py | 13 +++++++++---- 5 files changed, 14 insertions(+), 9 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 43fbec8c0..a45274266 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -354,12 +354,12 @@ class LocalTrade(): LocalTrade.trades_open = [] LocalTrade.total_profit = 0 - def adjust_min_max_rates(self, current_price: float) -> None: + def adjust_min_max_rates(self, current_price: float, current_price_low: float) -> None: """ Adjust the max_rate and min_rate. """ self.max_rate = max(current_price, self.max_rate or self.open_rate) - self.min_rate = min(current_price, self.min_rate or self.open_rate) + self.min_rate = min(current_price_low, self.min_rate or self.open_rate) def _set_new_stoploss(self, new_loss: float, stoploss: float): """Assign new stop value""" diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index d4f10301a..bb8980a53 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -568,7 +568,7 @@ class IStrategy(ABC, HyperStrategyMixin): current_rate = rate current_profit = trade.calc_profit_ratio(current_rate) - trade.adjust_min_max_rates(high or current_rate) + trade.adjust_min_max_rates(high or current_rate, low or current_rate) stoplossflag = self.stop_loss_reached(current_rate=current_rate, trade=trade, current_time=date, current_profit=current_profit, diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index ff9b81c30..57b2b8733 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -584,7 +584,7 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None: 'initial_stop_loss_ratio': [-0.1, -0.1], 'stop_loss_abs': [0.0940005, 0.09272236], 'stop_loss_ratio': [-0.1, -0.1], - 'min_rate': [0.1038, 0.10302485], + 'min_rate': [0.10370188, 0.10300000000000001], 'max_rate': [0.10501, 0.1038888], 'is_open': [False, False], 'buy_tag': [None, None], diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 686a36c96..cb4b8bd63 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -398,7 +398,7 @@ def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, traili exchange='binance', open_rate=1, ) - trade.adjust_min_max_rates(trade.open_rate) + trade.adjust_min_max_rates(trade.open_rate, trade.open_rate) strategy.trailing_stop = trailing strategy.trailing_stop_positive = -0.05 strategy.use_custom_stoploss = custom diff --git a/tests/test_persistence.py b/tests/test_persistence.py index f7bcad806..105cee23a 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -799,25 +799,30 @@ def test_adjust_min_max_rates(fee): open_rate=1, ) - trade.adjust_min_max_rates(trade.open_rate) + trade.adjust_min_max_rates(trade.open_rate, trade.open_rate) assert trade.max_rate == 1 assert trade.min_rate == 1 # check min adjusted, max remained - trade.adjust_min_max_rates(0.96) + trade.adjust_min_max_rates(0.96, 0.96) assert trade.max_rate == 1 assert trade.min_rate == 0.96 # check max adjusted, min remains - trade.adjust_min_max_rates(1.05) + trade.adjust_min_max_rates(1.05, 1.05) assert trade.max_rate == 1.05 assert trade.min_rate == 0.96 # current rate "in the middle" - no adjustment - trade.adjust_min_max_rates(1.03) + trade.adjust_min_max_rates(1.03, 1.03) assert trade.max_rate == 1.05 assert trade.min_rate == 0.96 + # current rate "in the middle" - no adjustment + trade.adjust_min_max_rates(1.10, 0.91) + assert trade.max_rate == 1.10 + assert trade.min_rate == 0.91 + @pytest.mark.usefixtures("init_persistence") @pytest.mark.parametrize('use_db', [True, False]) From 3f160c71443281d4118a79ee5c11af3891c384d5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 10 Aug 2021 07:09:38 +0200 Subject: [PATCH 040/100] Cache dataframe before cutting the first candle This allows providing the "current closed" candle in all cases. --- freqtrade/optimize/backtesting.py | 6 +++--- tests/optimize/test_backtesting.py | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 5efb5101f..fce27d39b 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -246,11 +246,11 @@ class Backtesting: if has_buy_tag: df_analyzed.loc[:, 'buy_tag'] = df_analyzed.loc[:, 'buy_tag'].shift(1) - df_analyzed.drop(df_analyzed.head(1).index, inplace=True) - # Update dataprovider cache self.dataprovider._set_cached_df(pair, self.timeframe, df_analyzed) + df_analyzed = df_analyzed.drop(df_analyzed.head(1).index) + # Convert from Pandas to list for performance reasons # (Looping Pandas is slow.) data[pair] = df_analyzed[headers].values.tolist() @@ -478,9 +478,9 @@ class Backtesting: if row[DATE_IDX] > tmp: continue - self.dataprovider._set_dataframe_max_index(row_index) row_index += 1 indexes[pair] = row_index + self.dataprovider._set_dataframe_max_index(row_index) # without positionstacking, we can only have one open trade per pair. # max_open_trades must be respected diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 57b2b8733..998b2d837 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -742,9 +742,9 @@ def test_backtest_alternate_buy_sell(default_conf, fee, mocker, testdatadir): # 100 buys signals results = result['results'] assert len(results) == 100 - # Cached data should be 199 (missing 1 candle at the start) + # Cached data should be 200 analyzed_df = backtesting.dataprovider.get_analyzed_dataframe('UNITTEST/BTC', '1m')[0] - assert len(analyzed_df) == 199 + assert len(analyzed_df) == 200 # Expect last candle to be 1 below end date (as the last candle is assumed as "incomplete" # during backtesting) expected_last_candle_date = backtest_conf['end_date'] - timedelta(minutes=1) @@ -807,11 +807,11 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir) assert len(evaluate_result_multi(results['results'], '5m', 3)) == 0 # Cached data correctly removed amounts - offset = 2 if tres == 0 else 1 + offset = 1 if tres == 0 else 0 removed_candles = len(data[pair]) - offset - backtesting.strategy.startup_candle_count assert len(backtesting.dataprovider.get_analyzed_dataframe(pair, '5m')[0]) == removed_candles assert len(backtesting.dataprovider.get_analyzed_dataframe( - 'NXT/BTC', '5m')[0]) == len(data['NXT/BTC']) - 2 - backtesting.strategy.startup_candle_count + 'NXT/BTC', '5m')[0]) == len(data['NXT/BTC']) - 1 - backtesting.strategy.startup_candle_count backtest_conf = { 'processed': processed, From 039d6384edf79d87556f5fa66b5917d1a77f9a6b Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 10 Aug 2021 09:48:26 +0200 Subject: [PATCH 041/100] Stream hyperopt-result in small batches Avoiding memory-exhaustion on huge hyperopt results closes #5305 closes #5149 --- freqtrade/optimize/hyperopt_epoch_filters.py | 12 ++--- freqtrade/optimize/hyperopt_tools.py | 54 +++++++++++--------- tests/commands/test_commands.py | 24 +++++++-- tests/optimize/test_hyperopt_tools.py | 24 +++++++-- 4 files changed, 78 insertions(+), 36 deletions(-) diff --git a/freqtrade/optimize/hyperopt_epoch_filters.py b/freqtrade/optimize/hyperopt_epoch_filters.py index b70db94af..80cc89d4b 100644 --- a/freqtrade/optimize/hyperopt_epoch_filters.py +++ b/freqtrade/optimize/hyperopt_epoch_filters.py @@ -7,7 +7,7 @@ from freqtrade.exceptions import OperationalException logger = logging.getLogger(__name__) -def hyperopt_filter_epochs(epochs: List, filteroptions: dict) -> List: +def hyperopt_filter_epochs(epochs: List, filteroptions: dict, log: bool = True) -> List: """ Filter our items from the list of hyperopt results """ @@ -24,11 +24,11 @@ def hyperopt_filter_epochs(epochs: List, filteroptions: dict) -> List: epochs = _hyperopt_filter_epochs_profit(epochs, filteroptions) epochs = _hyperopt_filter_epochs_objective(epochs, filteroptions) - - logger.info(f"{len(epochs)} " + - ("best " if filteroptions['only_best'] else "") + - ("profitable " if filteroptions['only_profitable'] else "") + - "epochs found.") + if log: + logger.info(f"{len(epochs)} " + + ("best " if filteroptions['only_best'] else "") + + ("profitable " if filteroptions['only_profitable'] else "") + + "epochs found.") return epochs diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index 0bb6aba15..b2e024f65 100755 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -4,7 +4,7 @@ import logging from copy import deepcopy from datetime import datetime, timezone from pathlib import Path -from typing import Any, Dict, List, Optional, Tuple +from typing import Any, Dict, Iterator, List, Optional, Tuple import numpy as np import rapidjson @@ -90,37 +90,33 @@ class HyperoptTools(): return any(s in config['spaces'] for s in [space, 'all', 'default']) @staticmethod - def _read_results(results_file: Path) -> List: + def _read_results(results_file: Path, batch_size: int = 10) -> Iterator[List[Any]]: """ - Read hyperopt results from file + Stream hyperopt results from file """ import rapidjson logger.info(f"Reading epochs from '{results_file}'") with results_file.open('r') as f: - data = [rapidjson.loads(line) for line in f] - return data + data = [] + for line in f: + data += [rapidjson.loads(line)] + if len(data) >= batch_size: + yield data + data = [] + yield data @staticmethod - def load_previous_results(results_file: Path) -> List: - """ - Load data for epochs from the file if we have one - """ - epochs: List = [] + def _test_hyperopt_results_exist(results_file) -> bool: if results_file.is_file() and results_file.stat().st_size > 0: if results_file.suffix == '.pickle': raise OperationalException( "Legacy hyperopt results are no longer supported." "Please rerun hyperopt or use an older version to load this file." ) - else: - epochs = HyperoptTools._read_results(results_file) - # Detection of some old format, without 'is_best' field saved - if epochs[0].get('is_best') is None: - raise OperationalException( - "The file with HyperoptTools results is incompatible with this version " - "of Freqtrade and cannot be loaded.") - logger.info(f"Loaded {len(epochs)} previous evaluations from disk.") - return epochs + return True + else: + # No file found. + return False @staticmethod def load_filtered_results(results_file: Path, config: Dict[str, Any]) -> Tuple[List, int]: @@ -138,12 +134,24 @@ class HyperoptTools(): 'filter_min_objective': config.get('hyperopt_list_min_objective', None), 'filter_max_objective': config.get('hyperopt_list_max_objective', None), } + if not HyperoptTools._test_hyperopt_results_exist(results_file): + # No file found. + return [], 0 - # Previous evaluations - epochs = HyperoptTools.load_previous_results(results_file) - total_epochs = len(epochs) + epochs = [] + total_epochs = 0 + for epochs_tmp in HyperoptTools._read_results(results_file): + if total_epochs == 0 and epochs_tmp[0].get('is_best') is None: + raise OperationalException( + "The file with HyperoptTools results is incompatible with this version " + "of Freqtrade and cannot be loaded.") + total_epochs += len(epochs_tmp) + epochs += hyperopt_filter_epochs(epochs_tmp, filteroptions, log=False) - epochs = hyperopt_filter_epochs(epochs, filteroptions) + logger.info(f"Loaded {total_epochs} previous evaluations from disk.") + + # Final filter run ... + epochs = hyperopt_filter_epochs(epochs, filteroptions, log=True) return epochs, total_epochs diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index 80dd04b27..fc5101979 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -941,8 +941,16 @@ def test_start_test_pairlist(mocker, caplog, tickers, default_conf, capsys): def test_hyperopt_list(mocker, capsys, caplog, saved_hyperopt_results, tmpdir): csv_file = Path(tmpdir) / "test.csv" mocker.patch( - 'freqtrade.optimize.hyperopt_tools.HyperoptTools.load_previous_results', - MagicMock(return_value=saved_hyperopt_results) + 'freqtrade.optimize.hyperopt_tools.HyperoptTools._test_hyperopt_results_exist', + return_value=True + ) + + def fake_iterator(*args, **kwargs): + yield from [saved_hyperopt_results] + + mocker.patch( + 'freqtrade.optimize.hyperopt_tools.HyperoptTools._read_results', + side_effect=fake_iterator ) args = [ @@ -1175,8 +1183,16 @@ def test_hyperopt_list(mocker, capsys, caplog, saved_hyperopt_results, tmpdir): def test_hyperopt_show(mocker, capsys, saved_hyperopt_results): mocker.patch( - 'freqtrade.optimize.hyperopt_tools.HyperoptTools.load_previous_results', - MagicMock(return_value=saved_hyperopt_results) + 'freqtrade.optimize.hyperopt_tools.HyperoptTools._test_hyperopt_results_exist', + return_value=True + ) + + def fake_iterator(*args, **kwargs): + yield from [saved_hyperopt_results] + + mocker.patch( + 'freqtrade.optimize.hyperopt_tools.HyperoptTools._read_results', + side_effect=fake_iterator ) mocker.patch('freqtrade.commands.hyperopt_commands.show_backtest_result') diff --git a/tests/optimize/test_hyperopt_tools.py b/tests/optimize/test_hyperopt_tools.py index d59a44da7..cbcb13384 100644 --- a/tests/optimize/test_hyperopt_tools.py +++ b/tests/optimize/test_hyperopt_tools.py @@ -20,9 +20,14 @@ def create_results() -> List[Dict]: def test_save_results_saves_epochs(hyperopt, tmpdir, caplog) -> None: + + hyperopt.results_file = Path(tmpdir / 'ut_results.fthypt') + + hyperopt_epochs = HyperoptTools.load_filtered_results(hyperopt.results_file, {}) + assert hyperopt_epochs == ([], 0) + # Test writing to temp dir and reading again epochs = create_results() - hyperopt.results_file = Path(tmpdir / 'ut_results.fthypt') caplog.set_level(logging.DEBUG) @@ -33,15 +38,28 @@ def test_save_results_saves_epochs(hyperopt, tmpdir, caplog) -> None: hyperopt._save_result(epochs[0]) assert log_has(f"2 epochs saved to '{hyperopt.results_file}'.", caplog) - hyperopt_epochs = HyperoptTools.load_previous_results(hyperopt.results_file) + hyperopt_epochs = HyperoptTools.load_filtered_results(hyperopt.results_file, {}) assert len(hyperopt_epochs) == 2 + assert hyperopt_epochs[1] == 2 + assert len(hyperopt_epochs[0]) == 2 + + result_gen = HyperoptTools._read_results(hyperopt.results_file, 1) + epoch = next(result_gen) + assert len(epoch) == 1 + assert epoch[0] == epochs[0] + epoch = next(result_gen) + assert len(epoch) == 1 + epoch = next(result_gen) + assert len(epoch) == 0 + with pytest.raises(StopIteration): + next(result_gen) def test_load_previous_results2(mocker, testdatadir, caplog) -> None: results_file = testdatadir / 'hyperopt_results_SampleStrategy.pickle' with pytest.raises(OperationalException, match=r"Legacy hyperopt results are no longer supported.*"): - HyperoptTools.load_previous_results(results_file) + HyperoptTools.load_filtered_results(results_file, {}) @pytest.mark.parametrize("spaces, expected_results", [ From 65d025923d9453b2173de63c898232847e31ad7a Mon Sep 17 00:00:00 2001 From: ipqhjjybj <250657661@qq.com> Date: Wed, 11 Aug 2021 14:35:16 +0800 Subject: [PATCH 042/100] add code --- freqtrade/optimize/backtesting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index fce27d39b..a8fd9c04a 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -504,7 +504,7 @@ class Backtesting: open_trades[pair].append(trade) LocalTrade.add_bt_trade(trade) - for trade in open_trades[pair]: + for trade in list(open_trades[pair]): # also check the buying candle for sell conditions. trade_entry = self._get_sell_trade_entry(trade, row) # Sell occurred From 61c076563fb7c778b93d9e056f5403423aa9406f Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 Aug 2021 12:11:29 +0200 Subject: [PATCH 043/100] Add max-slippage limiting for dry-run orders to avoid insane market order fills --- freqtrade/exchange/exchange.py | 13 ++++++++++++- tests/exchange/test_exchange.py | 23 ++++++++++++++--------- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index c6f60e08a..cde643cff 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -618,6 +618,8 @@ class Exchange: if self.exchange_has('fetchL2OrderBook'): ob = self.fetch_l2_order_book(pair, 20) ob_type = 'asks' if side == 'buy' else 'bids' + slippage = 0.05 + max_slippage_val = rate * ((1 + slippage) if side == 'buy' else (1 - slippage)) remaining_amount = amount filled_amount = 0 @@ -626,7 +628,9 @@ class Exchange: book_entry_coin_volume = book_entry[1] if remaining_amount > 0: if remaining_amount < book_entry_coin_volume: + # Orderbook at this slot bigger than remaining amount filled_amount += remaining_amount * book_entry_price + break else: filled_amount += book_entry_coin_volume * book_entry_price remaining_amount -= book_entry_coin_volume @@ -635,7 +639,14 @@ class Exchange: else: # If remaining_amount wasn't consumed completely (break was not called) filled_amount += remaining_amount * book_entry_price - forecast_avg_filled_price = filled_amount / amount + forecast_avg_filled_price = max(filled_amount, 0) / amount + # Limit max. slippage to specified value + if side == 'buy': + forecast_avg_filled_price = min(forecast_avg_filled_price, max_slippage_val) + + else: + forecast_avg_filled_price = max(forecast_avg_filled_price, max_slippage_val) + return self.price_to_precision(pair, forecast_avg_filled_price) return rate diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index a3ebbe8bd..9ac9f84e5 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -984,16 +984,21 @@ def test_create_dry_run_order_limit_fill(default_conf, mocker, side, startprice, assert order['fee'] -@pytest.mark.parametrize("side,amount,endprice", [ - ("buy", 1, 25.566), - ("buy", 100, 25.5672), # Requires interpolation - ("buy", 1000, 25.575), # More than orderbook return - ("sell", 1, 25.563), - ("sell", 100, 25.5625), # Requires interpolation - ("sell", 1000, 25.5555), # More than orderbook return +@pytest.mark.parametrize("side,rate,amount,endprice", [ + # spread is 25.263-25.266 + ("buy", 25.564, 1, 25.566), + ("buy", 25.564, 100, 25.5672), # Requires interpolation + ("buy", 25.590, 100, 25.5672), # Price above spread ... average is lower + ("buy", 25.564, 1000, 25.575), # More than orderbook return + ("buy", 24.000, 100000, 25.200), # Run into max_slippage of 5% + ("sell", 25.564, 1, 25.563), + ("sell", 25.564, 100, 25.5625), # Requires interpolation + ("sell", 25.510, 100, 25.5625), # price below spread - average is higher + ("sell", 25.564, 1000, 25.5555), # More than orderbook return + ("sell", 27, 10000, 25.65), # max-slippage 5% ]) @pytest.mark.parametrize("exchange_name", EXCHANGES) -def test_create_dry_run_order_market_fill(default_conf, mocker, side, amount, endprice, +def test_create_dry_run_order_market_fill(default_conf, mocker, side, rate, amount, endprice, exchange_name, order_book_l2_usd): default_conf['dry_run'] = True exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) @@ -1003,7 +1008,7 @@ def test_create_dry_run_order_market_fill(default_conf, mocker, side, amount, en ) order = exchange.create_dry_run_order( - pair='LTC/USDT', ordertype='market', side=side, amount=amount, rate=25.5) + pair='LTC/USDT', ordertype='market', side=side, amount=amount, rate=rate) assert 'id' in order assert f'dry_run_{side}_' in order["id"] assert order["side"] == side From f6267c75143b35c0270428f70d7936f7880fc917 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Wed, 11 Aug 2021 10:18:25 +0300 Subject: [PATCH 044/100] Fix buy_tag not being saved to trade object. Column is mistakenly excluded because advise_buy() creating this column runs after code detecting presence of buy_tag column. --- freqtrade/optimize/backtesting.py | 10 +++------- tests/optimize/__init__.py | 2 ++ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index fce27d39b..06464d40b 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -218,7 +218,7 @@ class Backtesting: """ # Every change to this headers list must evaluate further usages of the resulting tuple # and eventually change the constants for indexes at the top - headers = ['date', 'buy', 'open', 'close', 'sell', 'low', 'high'] + headers = ['date', 'buy', 'open', 'close', 'sell', 'low', 'high', 'buy_tag'] data: Dict = {} self.progress.init_step(BacktestState.CONVERT, len(processed)) @@ -226,13 +226,10 @@ class Backtesting: for pair, pair_data in processed.items(): self.check_abort() self.progress.increment() - has_buy_tag = 'buy_tag' in pair_data - headers = headers + ['buy_tag'] if has_buy_tag else headers if not pair_data.empty: pair_data.loc[:, 'buy'] = 0 # cleanup if buy_signal is exist pair_data.loc[:, 'sell'] = 0 # cleanup if sell_signal is exist - if has_buy_tag: - pair_data.loc[:, 'buy_tag'] = None # cleanup if buy_tag is exist + pair_data.loc[:, 'buy_tag'] = None # cleanup if buy_tag is exist df_analyzed = self.strategy.advise_sell( self.strategy.advise_buy(pair_data, {'pair': pair}), {'pair': pair}).copy() @@ -243,8 +240,7 @@ class Backtesting: # from the previous candle df_analyzed.loc[:, 'buy'] = df_analyzed.loc[:, 'buy'].shift(1) df_analyzed.loc[:, 'sell'] = df_analyzed.loc[:, 'sell'].shift(1) - if has_buy_tag: - df_analyzed.loc[:, 'buy_tag'] = df_analyzed.loc[:, 'buy_tag'].shift(1) + df_analyzed.loc[:, 'buy_tag'] = df_analyzed.loc[:, 'buy_tag'].shift(1) # Update dataprovider cache self.dataprovider._set_cached_df(pair, self.timeframe, df_analyzed) diff --git a/tests/optimize/__init__.py b/tests/optimize/__init__.py index f29d8d585..6ad2d300b 100644 --- a/tests/optimize/__init__.py +++ b/tests/optimize/__init__.py @@ -52,4 +52,6 @@ def _build_backtest_dataframe(data): # Ensure floats are in place for column in ['open', 'high', 'low', 'close', 'volume']: frame[column] = frame[column].astype('float64') + if 'buy_tag' not in columns: + frame['buy_tag'] = None return frame From fad253ad5141978b5d35d878414feeead837fab8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 12 Aug 2021 06:53:55 +0200 Subject: [PATCH 045/100] Version bump ccxt to 1.54.74 closes #5401 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 60175c12f..0e107d8e0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.21.1 pandas==1.3.1 -ccxt==1.54.62 +ccxt==1.54.74 # Pin cryptography for now due to rust build errors with piwheels cryptography==3.4.7 aiohttp==3.7.4.post0 From e03784d98dc016790cb42eb194adf2c2be752091 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 12 Aug 2021 05:02:36 +0000 Subject: [PATCH 046/100] Fix filled exception closes #5404 --- freqtrade/persistence/models.py | 2 +- tests/test_persistence.py | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index a45274266..5eaca7966 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -161,7 +161,7 @@ class Order(_DECL_BASE): self.ft_is_open = True if self.status in ('closed', 'canceled', 'cancelled'): self.ft_is_open = False - if order.get('filled', 0) > 0: + if (order.get('filled', 0.0) or 0.0) > 0: self.order_filled_date = datetime.now(timezone.utc) self.order_update_date = datetime.now(timezone.utc) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 105cee23a..d036b045e 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -1224,6 +1224,11 @@ def test_update_order_from_ccxt(caplog): assert o.ft_is_open assert o.order_filled_date is None + # Order is unfilled, "filled" not set + # https://github.com/freqtrade/freqtrade/issues/5404 + ccxt_order.update({'filled': None, 'remaining': 20.0, 'status': 'canceled'}) + o.update_from_ccxt_object(ccxt_order) + # Order has been closed ccxt_order.update({'filled': 20.0, 'remaining': 0.0, 'status': 'closed'}) o.update_from_ccxt_object(ccxt_order) From ae11be39706b37eb6733b12cee814cdf351b53d9 Mon Sep 17 00:00:00 2001 From: axel Date: Thu, 12 Aug 2021 14:47:01 -0400 Subject: [PATCH 047/100] manage None or string value returned by custom_entry_price and add unit test for those cases --- freqtrade/freqtradebot.py | 12 ++++++++---- tests/test_freqtradebot.py | 24 ++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 99f5d2894..99fe1c5a3 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -479,13 +479,17 @@ class FreqtradeBot(LoggingMixin): buy_limit_requested = price else: # Calculate price - buy_limit_requested = self.exchange.get_rate(pair, refresh=True, side="buy") + proposed_buy_rate = self.exchange.get_rate(pair, refresh=True, side="buy") custom_entry_price = strategy_safe_wrapper(self.strategy.custom_entry_price, - default_retval=buy_limit_requested)( + default_retval=proposed_buy_rate)( pair=pair, current_time=datetime.now(timezone.utc), - proposed_rate=buy_limit_requested) + proposed_rate=proposed_buy_rate) - buy_limit_requested = custom_entry_price + if custom_entry_price and (isinstance(custom_entry_price, int) + or isinstance(custom_entry_price, float)): + buy_limit_requested = custom_entry_price + else: + buy_limit_requested = proposed_buy_rate if not buy_limit_requested: raise PricingError('Could not determine buy price.') diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index c73e51dec..69a4fa530 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -913,6 +913,30 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order, limit_buy_order assert trade assert trade.open_rate_requested == 0.77 + # In case of custom entry price set to None + limit_buy_order['status'] = 'open' + limit_buy_order['id'] = '5567' + freqtrade.strategy.custom_entry_price = lambda **kwargs: None + + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_rate=MagicMock(return_value=10), + ) + + assert freqtrade.execute_buy(pair, stake_amount) + trade = Trade.query.all()[7] + assert trade + assert trade.open_rate_requested == 10 + + # In case of custom entry price not float type + limit_buy_order['status'] = 'open' + limit_buy_order['id'] = '5568' + freqtrade.strategy.custom_entry_price = lambda **kwargs: "string price" + assert freqtrade.execute_buy(pair, stake_amount) + trade = Trade.query.all()[8] + assert trade + assert trade.open_rate_requested == 10 + def test_execute_buy_confirm_error(mocker, default_conf, fee, limit_buy_order) -> None: freqtrade = get_patched_freqtradebot(mocker, default_conf) From b098ce4e76abe78f27fee9be8860399ebc831f7a Mon Sep 17 00:00:00 2001 From: axel Date: Thu, 12 Aug 2021 15:13:14 -0400 Subject: [PATCH 048/100] add function get_valid_price to validate type of custom entry or exit price and use default proposed price if invalid --- freqtrade/freqtradebot.py | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 99fe1c5a3..99670d612 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -485,11 +485,7 @@ class FreqtradeBot(LoggingMixin): pair=pair, current_time=datetime.now(timezone.utc), proposed_rate=proposed_buy_rate) - if custom_entry_price and (isinstance(custom_entry_price, int) - or isinstance(custom_entry_price, float)): - buy_limit_requested = custom_entry_price - else: - buy_limit_requested = proposed_buy_rate + buy_limit_requested = self.get_valid_price(custom_entry_price, proposed_buy_rate) if not buy_limit_requested: raise PricingError('Could not determine buy price.') @@ -1087,12 +1083,15 @@ class FreqtradeBot(LoggingMixin): limit = trade.stop_loss # set custom_exit_price if available + proposed_limit_rate = limit current_profit = trade.calc_profit_ratio(limit) - limit = strategy_safe_wrapper(self.strategy.custom_exit_price, - default_retval=limit)( + custom_exit_price = strategy_safe_wrapper(self.strategy.custom_exit_price, + default_retval=proposed_limit_rate)( pair=trade.pair, trade=trade, current_time=datetime.now(timezone.utc), - proposed_rate=limit, current_profit=current_profit) + proposed_rate=proposed_limit_rate, current_profit=current_profit) + + limit = self.get_valid_price(custom_exit_price, proposed_limit_rate) # First cancelling stoploss on exchange ... if self.strategy.order_types.get('stoploss_on_exchange') and trade.stoploss_order_id: @@ -1393,3 +1392,17 @@ class FreqtradeBot(LoggingMixin): amount=amount, fee_abs=fee_abs) else: return amount + + def get_valid_price(self, custom_price: float, proposed_price: float) -> float: + """ + Return the valid price. + Check if the custom price is of the good type if not return proposed_price + :return: valid price for the order + """ + if custom_price and (isinstance(custom_price, int) + or isinstance(custom_price, float)): + valid_price = custom_price + else: + valid_price = proposed_price + + return valid_price From dbf7f34ecb180672a79454817725b15d058e4480 Mon Sep 17 00:00:00 2001 From: axel Date: Thu, 12 Aug 2021 15:30:49 -0400 Subject: [PATCH 049/100] add unit test to function get_valid_price --- freqtrade/freqtradebot.py | 10 +++++++--- tests/test_freqtradebot.py | 24 ++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 99670d612..2225ddd89 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1399,9 +1399,13 @@ class FreqtradeBot(LoggingMixin): Check if the custom price is of the good type if not return proposed_price :return: valid price for the order """ - if custom_price and (isinstance(custom_price, int) - or isinstance(custom_price, float)): - valid_price = custom_price + if custom_price: + if isinstance(custom_price, int): + valid_price = float(custom_price) + elif isinstance(custom_price, float): + valid_price = custom_price + else: + valid_price = proposed_price else: valid_price = proposed_price diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 69a4fa530..a67f5b290 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -4588,3 +4588,27 @@ def test_refind_lost_order(mocker, default_conf, fee, caplog): freqtrade.refind_lost_order(trades[4]) assert log_has(f"Error updating {order['id']}.", caplog) + + +def test_get_valid_price(mocker, default_conf) -> None: + patch_RPCManager(mocker) + patch_exchange(mocker) + freqtrade = FreqtradeBot(default_conf) + + custom_price_string = "10" + custom_price_float = 10.0 + custom_price_int = 10 + + proposed_price = 12.2 + + valid_price_from_string = freqtrade.get_valid_price(custom_price_string, proposed_price) + valid_price_from_int = freqtrade.get_valid_price(custom_price_int, proposed_price) + valid_price_from_float = freqtrade.get_valid_price(custom_price_float, proposed_price) + + assert isinstance(valid_price_from_string, float) + assert isinstance(valid_price_from_int, float) + assert isinstance(valid_price_from_float, float) + + assert valid_price_from_string == proposed_price + assert valid_price_from_int == custom_price_int + assert valid_price_from_float == custom_price_float From 20cc60bfde456dafd94bc59ab797018f62fb20ad Mon Sep 17 00:00:00 2001 From: axel Date: Fri, 13 Aug 2021 11:06:15 -0400 Subject: [PATCH 050/100] update get_valid_price function and test cases to handle inputs with try catch --- freqtrade/freqtradebot.py | 6 ++---- tests/test_freqtradebot.py | 6 +++++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 2225ddd89..9a1b2ab0c 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1400,11 +1400,9 @@ class FreqtradeBot(LoggingMixin): :return: valid price for the order """ if custom_price: - if isinstance(custom_price, int): + try: valid_price = float(custom_price) - elif isinstance(custom_price, float): - valid_price = custom_price - else: + except ValueError: valid_price = proposed_price else: valid_price = proposed_price diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index a67f5b290..a475ced48 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -4596,19 +4596,23 @@ def test_get_valid_price(mocker, default_conf) -> None: freqtrade = FreqtradeBot(default_conf) custom_price_string = "10" + custom_price_badstring = "10abc" custom_price_float = 10.0 custom_price_int = 10 proposed_price = 12.2 valid_price_from_string = freqtrade.get_valid_price(custom_price_string, proposed_price) + valid_price_from_badstring = freqtrade.get_valid_price(custom_price_badstring, proposed_price) valid_price_from_int = freqtrade.get_valid_price(custom_price_int, proposed_price) valid_price_from_float = freqtrade.get_valid_price(custom_price_float, proposed_price) assert isinstance(valid_price_from_string, float) + assert isinstance(valid_price_from_badstring, float) assert isinstance(valid_price_from_int, float) assert isinstance(valid_price_from_float, float) - assert valid_price_from_string == proposed_price + assert valid_price_from_string == custom_price_float + assert valid_price_from_badstring == proposed_price assert valid_price_from_int == custom_price_int assert valid_price_from_float == custom_price_float From 0a6c0c429ae5fb354a06d8409231216aa387e0f6 Mon Sep 17 00:00:00 2001 From: axel Date: Fri, 13 Aug 2021 11:12:33 -0400 Subject: [PATCH 051/100] add a note concerning default custom entry or exit price in documentation --- docs/strategy-advanced.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index f59cb8ef5..17fdddc37 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -363,6 +363,9 @@ By default, freqtrade use the orderbook to automatically set an order price, you You can use this feature by creating a custom_entry_price function in your strategy file to customize entry prices and custom_exit_price for exits. +!!!Note +If your custom pricing function return None or an invalid value, a default entry or exit price will be chosen based on the current rate. + ### Custom order entry and exit price exemple ``` python from datetime import datetime, timedelta, timezone From c7147311f8d75635cff229ceccc88656903da045 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 13 Aug 2021 17:14:38 +0200 Subject: [PATCH 052/100] Fix json syntax error in config template --- freqtrade/templates/base_config.json.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/templates/base_config.json.j2 b/freqtrade/templates/base_config.json.j2 index 03a6c4855..a5782f7cd 100644 --- a/freqtrade/templates/base_config.json.j2 +++ b/freqtrade/templates/base_config.json.j2 @@ -25,7 +25,7 @@ "ask_strategy": { "price_side": "ask", "use_order_book": true, - "order_book_top": 1, + "order_book_top": 1 }, {{ exchange | indent(4) }}, "pairlists": [ From db5a9443967eae8c0ed97229d5a510a3ffe14715 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 14 Aug 2021 08:46:19 +0200 Subject: [PATCH 053/100] Cleanup GHA node after building images --- build_helpers/publish_docker_arm64.sh | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/build_helpers/publish_docker_arm64.sh b/build_helpers/publish_docker_arm64.sh index 08793d339..e7b69b2dc 100755 --- a/build_helpers/publish_docker_arm64.sh +++ b/build_helpers/publish_docker_arm64.sh @@ -74,7 +74,5 @@ fi docker images -if [ $? -ne 0 ]; then - echo "failed building image" - return 1 -fi +# Cleanup old images from arm64 node. +docker image prune -a --force --filter "until=24h" From bb472ff98b06a8ae4059b742e56b22f1d2aca244 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 14 Aug 2021 08:47:14 +0200 Subject: [PATCH 054/100] Improve new-exchange documentation --- docs/developer.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/developer.md b/docs/developer.md index dd56a367c..bd138212b 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -240,11 +240,18 @@ The `IProtection` parent class provides a helper method for this in `calculate_l !!! Note This section is a Work in Progress and is not a complete guide on how to test a new exchange with Freqtrade. +!!! Note + Make sure to use an up-to-date version of CCXT before running any of the below tests. + You can get the latest version of ccxt by running `pip install -U ccxt` with activated virtual environment. + Native docker is not supported for these tests, however the available dev-container will support all required actions and eventually necessary changes. + Most exchanges supported by CCXT should work out of the box. To quickly test the public endpoints of an exchange, add a configuration for your exchange to `test_ccxt_compat.py` and run these tests with `pytest --longrun tests/exchange/test_ccxt_compat.py`. Completing these tests successfully a good basis point (it's a requirement, actually), however these won't guarantee correct exchange functioning, as this only tests public endpoints, but no private endpoint (like generate order or similar). +Also try to use `freqtrade download-data` for an extended timerange and verify that the data downloaded correctly (no holes, the specified timerange was actually downloaded). + ### Stoploss On Exchange Check if the new exchange supports Stoploss on Exchange orders through their API. From 0f7ddabec80428e80f036618e65d4e72d7605af7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 14 Aug 2021 09:05:03 +0200 Subject: [PATCH 055/100] Slightly reword documentation --- docs/bot-basics.md | 5 +++-- docs/strategy-advanced.md | 24 +++++++++++++++--------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/docs/bot-basics.md b/docs/bot-basics.md index 943af0362..b34594f46 100644 --- a/docs/bot-basics.md +++ b/docs/bot-basics.md @@ -36,11 +36,12 @@ By default, loop runs every few seconds (`internals.process_throttle_secs`) and * Calls `check_sell_timeout()` strategy callback for open sell orders. * Verifies existing positions and eventually places sell orders. * Considers stoploss, ROI and sell-signal. - * Determine sell-price based on `ask_strategy` configuration setting. + * Determine sell-price based on `ask_strategy` configuration setting or by using the `custom_exit_price()` callback. * Before a sell order is placed, `confirm_trade_exit()` strategy callback is called. * Check if trade-slots are still available (if `max_open_trades` is reached). * Verifies buy signal trying to enter new positions. - * Determine buy-price based on `bid_strategy` configuration setting. + * Determine buy-price based on `bid_strategy` configuration setting, or by using the `custom_entry_price()` callback. + * Determine stake size by calling the `custom_stake_amount()` callback. * Before a buy order is placed, `confirm_trade_entry()` strategy callback is called. This loop will be repeated again and again until the bot is stopped. diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 17fdddc37..e53f20693 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -359,14 +359,15 @@ See [Dataframe access](#dataframe-access) for more information about dataframe u ## Custom order price rules -By default, freqtrade use the orderbook to automatically set an order price, you also have the option to create custom order prices based on your strategy. +By default, freqtrade use the orderbook to automatically set an order price([Relevant documentation](configuration.md#prices-used-for-orders)), you also have the option to create custom order prices based on your strategy. -You can use this feature by creating a custom_entry_price function in your strategy file to customize entry prices and custom_exit_price for exits. +You can use this feature by creating a `custom_entry_price()` function in your strategy file to customize entry prices and custom_exit_price for exits. -!!!Note -If your custom pricing function return None or an invalid value, a default entry or exit price will be chosen based on the current rate. +!!! Note + If your custom pricing function return None or an invalid value, price will fall back to `proposed_rate`, which is based on the regular pricing configuration. + +### Custom order entry and exit price example -### Custom order entry and exit price exemple ``` python from datetime import datetime, timedelta, timezone from freqtrade.persistence import Trade @@ -380,9 +381,9 @@ class AwesomeStrategy(IStrategy): dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=pair, timeframe=self.timeframe) - proposed_entryprice = dataframe['bollinger_10_lowerband'].iat[-1] + new_entryprice = dataframe['bollinger_10_lowerband'].iat[-1] - return proposed_entryprice + return new_entryprice def custom_exit_price(self, pair: str, trade: Trade, current_time: datetime, proposed_rate: float, @@ -390,12 +391,17 @@ class AwesomeStrategy(IStrategy): dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=pair, timeframe=self.timeframe) - proposed_exitprice = dataframe['bollinger_10_upperband'].iat[-1] + new_exitprice = dataframe['bollinger_10_upperband'].iat[-1] - return proposed_exitprice + return new_exitprice ``` +!!! Warning + Modifying entry and exit prices will only work for limit orders. Depending on the price chosen, this can result in a lot of unfilled orders. + +!!! Warning "No backtesting support" + Custom entry-prices are currently not supported during backtesting. ## Custom order timeout rules From 123971d2713a65d51421f46958ac481e03d9f518 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 14 Aug 2021 16:02:12 +0200 Subject: [PATCH 056/100] Don't change passed in parameter variable --- freqtrade/rpc/api_server/api_v1.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index f2361fda8..7e613f184 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -223,11 +223,11 @@ def list_strategies(config=Depends(get_config)): @router.get('/strategy/{strategy}', response_model=StrategyResponse, tags=['strategy']) def get_strategy(strategy: str, config=Depends(get_config)): - config = deepcopy(config) + config_ = deepcopy(config) from freqtrade.resolvers.strategy_resolver import StrategyResolver try: - strategy_obj = StrategyResolver._load_strategy(strategy, config, - extra_dir=config.get('strategy_path')) + strategy_obj = StrategyResolver._load_strategy(strategy, config_, + extra_dir=config_.get('strategy_path')) except OperationalException: raise HTTPException(status_code=404, detail='Strategy not found') From 4f10a885290c5703e0f93e6880db8af757e20fca Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 Aug 2021 15:14:53 +0200 Subject: [PATCH 057/100] Reduce verbosity when incompatible pairs are detected --- freqtrade/plugins/pairlist/IPairList.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/freqtrade/plugins/pairlist/IPairList.py b/freqtrade/plugins/pairlist/IPairList.py index bfde2ace0..0155f918b 100644 --- a/freqtrade/plugins/pairlist/IPairList.py +++ b/freqtrade/plugins/pairlist/IPairList.py @@ -150,18 +150,20 @@ class IPairList(LoggingMixin, ABC): for pair in pairlist: # pair is not in the generated dynamic market or has the wrong stake currency if pair not in markets: - logger.warning(f"Pair {pair} is not compatible with exchange " - f"{self._exchange.name}. Removing it from whitelist..") + self.log_once(f"Pair {pair} is not compatible with exchange " + f"{self._exchange.name}. Removing it from whitelist..", + logger.warning) continue if not self._exchange.market_is_tradable(markets[pair]): - logger.warning(f"Pair {pair} is not tradable with Freqtrade." - "Removing it from whitelist..") + self.log_once(f"Pair {pair} is not tradable with Freqtrade." + "Removing it from whitelist..", logger.warning) continue if self._exchange.get_pair_quote_currency(pair) != self._config['stake_currency']: - logger.warning(f"Pair {pair} is not compatible with your stake currency " - f"{self._config['stake_currency']}. Removing it from whitelist..") + self.log_once(f"Pair {pair} is not compatible with your stake currency " + f"{self._config['stake_currency']}. Removing it from whitelist..", + logger.warning) continue # Check if market is active From ed6776c5cdb8d2472cb0ef3669e3b4b4fe987021 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 Aug 2021 17:06:16 +0200 Subject: [PATCH 058/100] Fix disappearing pairlist bug --- freqtrade/plugins/pairlist/VolumePairList.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/plugins/pairlist/VolumePairList.py b/freqtrade/plugins/pairlist/VolumePairList.py index 901fde2d0..8d01aeee1 100644 --- a/freqtrade/plugins/pairlist/VolumePairList.py +++ b/freqtrade/plugins/pairlist/VolumePairList.py @@ -126,7 +126,7 @@ class VolumePairList(IPairList): pairlist = [s['symbol'] for s in filtered_tickers] pairlist = self.filter_pairlist(pairlist, tickers) - self._pair_cache['pairlist'] = pairlist + self._pair_cache['pairlist'] = pairlist.copy() return pairlist From 322ea2481e7be80355199d803aab29bf5f52d827 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 Aug 2021 19:28:36 +0200 Subject: [PATCH 059/100] Add log-message for edge-case --- freqtrade/plugins/pairlist/rangestabilityfilter.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/plugins/pairlist/rangestabilityfilter.py b/freqtrade/plugins/pairlist/rangestabilityfilter.py index ef7f2cbcb..3e5a002ff 100644 --- a/freqtrade/plugins/pairlist/rangestabilityfilter.py +++ b/freqtrade/plugins/pairlist/rangestabilityfilter.py @@ -120,5 +120,6 @@ class RangeStabilityFilter(IPairList): logger.info) result = False self._pair_cache[pair] = result - + else: + self.log_once(f"Removed {pair} from whitelist, no candles found.", logger.info) return result From dda82765893d4474e4a90d0b274dfab36939c382 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 Aug 2021 20:01:54 +0200 Subject: [PATCH 060/100] Update documentation for sell_profit_offset As highlighed in #5393 --- docs/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index fd4806fe6..fab3004a5 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -105,7 +105,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `ask_strategy.order_book_top` | Bot will use the top N rate in Order Book "price_side" to sell. I.e. a value of 2 will allow the bot to pick the 2nd ask rate in [Order Book Asks](#sell-price-with-orderbook-enabled)
*Defaults to `1`.*
**Datatype:** Positive Integer | `use_sell_signal` | Use sell signals produced by the strategy in addition to the `minimal_roi`. [Strategy Override](#parameters-in-the-strategy).
*Defaults to `true`.*
**Datatype:** Boolean | `sell_profit_only` | Wait until the bot reaches `sell_profit_offset` before taking a sell decision. [Strategy Override](#parameters-in-the-strategy).
*Defaults to `false`.*
**Datatype:** Boolean -| `sell_profit_offset` | Sell-signal is only active above this value. [Strategy Override](#parameters-in-the-strategy).
*Defaults to `0.0`.*
**Datatype:** Float (as ratio) +| `sell_profit_offset` | Sell-signal is only active above this value. Only active in combination with `sell_profit_only=True`. [Strategy Override](#parameters-in-the-strategy).
*Defaults to `0.0`.*
**Datatype:** Float (as ratio) | `ignore_roi_if_buy_signal` | Do not sell if the buy signal is still active. This setting takes preference over `minimal_roi` and `use_sell_signal`. [Strategy Override](#parameters-in-the-strategy).
*Defaults to `false`.*
**Datatype:** Boolean | `ignore_buying_expired_candle_after` | Specifies the number of seconds until a buy signal is no longer used.
**Datatype:** Integer | `order_types` | Configure order-types depending on the action (`"buy"`, `"sell"`, `"stoploss"`, `"stoploss_on_exchange"`). [More information below](#understand-order_types). [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Dict From 37d4545123cbfbf5e52cddf703396a38105f9157 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Aug 2021 03:01:06 +0000 Subject: [PATCH 061/100] Bump ccxt from 1.54.74 to 1.55.6 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.54.74 to 1.55.6. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/exchanges.cfg) - [Commits](https://github.com/ccxt/ccxt/compare/1.54.74...1.55.6) --- updated-dependencies: - dependency-name: ccxt dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 0e107d8e0..3880e3ced 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.21.1 pandas==1.3.1 -ccxt==1.54.74 +ccxt==1.55.6 # Pin cryptography for now due to rust build errors with piwheels cryptography==3.4.7 aiohttp==3.7.4.post0 From e7b6a996dffbdffdfd9ab5a9de1ca87fdb5f91e5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Aug 2021 03:01:10 +0000 Subject: [PATCH 062/100] Bump mkdocs-material from 7.2.2 to 7.2.4 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 7.2.2 to 7.2.4. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/docs/changelog.md) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/7.2.2...7.2.4) --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- docs/requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 047821f2d..8fa7341c9 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,4 +1,4 @@ mkdocs==1.2.2 -mkdocs-material==7.2.2 +mkdocs-material==7.2.4 mdx_truly_sane_lists==1.2 pymdown-extensions==8.2 From 7125793249cb1cb6b97b2206fe8580c07150d04d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Aug 2021 03:01:16 +0000 Subject: [PATCH 063/100] Bump uvicorn from 0.14.0 to 0.15.0 Bumps [uvicorn](https://github.com/encode/uvicorn) from 0.14.0 to 0.15.0. - [Release notes](https://github.com/encode/uvicorn/releases) - [Changelog](https://github.com/encode/uvicorn/blob/master/CHANGELOG.md) - [Commits](https://github.com/encode/uvicorn/compare/0.14.0...0.15.0) --- updated-dependencies: - dependency-name: uvicorn dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 0e107d8e0..ae6b24eb5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -32,7 +32,7 @@ sdnotify==0.3.2 # API Server fastapi==0.68.0 -uvicorn==0.14.0 +uvicorn==0.15.0 pyjwt==2.1.0 aiofiles==0.7.0 From d8607b2ce880163027f6736702934c94949257f4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Aug 2021 03:01:18 +0000 Subject: [PATCH 064/100] Bump flake8-tidy-imports from 4.3.0 to 4.4.1 Bumps [flake8-tidy-imports](https://github.com/adamchainz/flake8-tidy-imports) from 4.3.0 to 4.4.1. - [Release notes](https://github.com/adamchainz/flake8-tidy-imports/releases) - [Changelog](https://github.com/adamchainz/flake8-tidy-imports/blob/main/HISTORY.rst) - [Commits](https://github.com/adamchainz/flake8-tidy-imports/compare/4.3.0...4.4.1) --- updated-dependencies: - dependency-name: flake8-tidy-imports dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 9629bbea1..b20cb6693 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -6,7 +6,7 @@ coveralls==3.2.0 flake8==3.9.2 flake8-type-annotations==0.1.0 -flake8-tidy-imports==4.3.0 +flake8-tidy-imports==4.4.1 mypy==0.910 pytest==6.2.4 pytest-asyncio==0.15.1 From a10fd6690675d481f26df78d0fa3f9147684140e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Aug 2021 03:01:21 +0000 Subject: [PATCH 065/100] Bump plotly from 5.1.0 to 5.2.1 Bumps [plotly](https://github.com/plotly/plotly.py) from 5.1.0 to 5.2.1. - [Release notes](https://github.com/plotly/plotly.py/releases) - [Changelog](https://github.com/plotly/plotly.py/blob/master/CHANGELOG.md) - [Commits](https://github.com/plotly/plotly.py/compare/v5.1.0...v5.2.1) --- updated-dependencies: - dependency-name: plotly dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements-plot.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-plot.txt b/requirements-plot.txt index e03fd4d66..d835ed5d9 100644 --- a/requirements-plot.txt +++ b/requirements-plot.txt @@ -1,5 +1,5 @@ # Include all requirements to run the bot. -r requirements.txt -plotly==5.1.0 +plotly==5.2.1 From 3878e5186e19a3f5fbe3f14c1b6300320891496e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Aug 2021 03:01:34 +0000 Subject: [PATCH 066/100] Bump numpy from 1.21.1 to 1.21.2 Bumps [numpy](https://github.com/numpy/numpy) from 1.21.1 to 1.21.2. - [Release notes](https://github.com/numpy/numpy/releases) - [Changelog](https://github.com/numpy/numpy/blob/main/doc/HOWTO_RELEASE.rst.txt) - [Commits](https://github.com/numpy/numpy/compare/v1.21.1...v1.21.2) --- updated-dependencies: - dependency-name: numpy dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 0e107d8e0..1d0951e6d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -numpy==1.21.1 +numpy==1.21.2 pandas==1.3.1 ccxt==1.54.74 From 108a6cb897342e4f436d41f845dc9d0198e58770 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Aug 2021 04:28:42 +0000 Subject: [PATCH 067/100] Bump pandas from 1.3.1 to 1.3.2 Bumps [pandas](https://github.com/pandas-dev/pandas) from 1.3.1 to 1.3.2. - [Release notes](https://github.com/pandas-dev/pandas/releases) - [Changelog](https://github.com/pandas-dev/pandas/blob/master/RELEASE.md) - [Commits](https://github.com/pandas-dev/pandas/compare/v1.3.1...v1.3.2) --- updated-dependencies: - dependency-name: pandas dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 863475d86..e93e6211f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ numpy==1.21.2 -pandas==1.3.1 +pandas==1.3.2 ccxt==1.55.6 # Pin cryptography for now due to rust build errors with piwheels From abddb3ef257e6046b65595b0da2d19145adebccc Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 16 Aug 2021 06:38:36 +0200 Subject: [PATCH 068/100] Add test for directory traversal --- tests/rpc/test_rpc_apiserver.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 1517b6fcc..edf5ce3c5 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -109,6 +109,10 @@ def test_api_ui_fallback(botclient): rc = client_get(client, "/something") assert rc.status_code == 200 + rc = client_get(client, '%2F%2F%2Fetc/passwd') + assert rc.status_code == 200 + assert '`freqtrade install-ui`' in rc.text + def test_api_ui_version(botclient, mocker): ftbot, client = botclient From 6b2ef36a567a4aba47113fb56dfa9377f70797ad Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 16 Aug 2021 06:45:43 +0200 Subject: [PATCH 069/100] Prevent directory traversal in UI Serving Checking for directory base closes #5427 --- freqtrade/rpc/api_server/web_ui.py | 7 +++++-- tests/rpc/test_rpc_apiserver.py | 1 + 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/api_server/web_ui.py b/freqtrade/rpc/api_server/web_ui.py index 76c8ed8f2..0168930cf 100644 --- a/freqtrade/rpc/api_server/web_ui.py +++ b/freqtrade/rpc/api_server/web_ui.py @@ -37,8 +37,11 @@ async def index_html(rest_of_path: str): if rest_of_path.startswith('api') or rest_of_path.startswith('.'): raise HTTPException(status_code=404, detail="Not Found") uibase = Path(__file__).parent / 'ui/installed/' - if (uibase / rest_of_path).is_file(): - return FileResponse(str(uibase / rest_of_path)) + filename = uibase / rest_of_path + # It's security relevant to check "relative_to". + # Without this, Directory-traversal is possible. + if filename.is_file() and filename.is_relative_to(uibase): + return FileResponse(str(filename)) index_file = uibase / 'index.html' if not index_file.is_file(): diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index edf5ce3c5..3d02e8188 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -109,6 +109,7 @@ def test_api_ui_fallback(botclient): rc = client_get(client, "/something") assert rc.status_code == 200 + # Test directory traversal rc = client_get(client, '%2F%2F%2Fetc/passwd') assert rc.status_code == 200 assert '`freqtrade install-ui`' in rc.text From 4b65206e6b8e19ef7696a8f7daaad3a441aa352e Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 16 Aug 2021 07:08:50 +0200 Subject: [PATCH 070/100] Add compat code for is_relative_to --- freqtrade/rpc/api_server/web_ui.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/api_server/web_ui.py b/freqtrade/rpc/api_server/web_ui.py index 0168930cf..9aae0afae 100644 --- a/freqtrade/rpc/api_server/web_ui.py +++ b/freqtrade/rpc/api_server/web_ui.py @@ -29,6 +29,15 @@ async def ui_version(): } +def is_relative_to(path, base) -> bool: + # Helper function simulating behaviour of is_relative_to, which was only added in python 3.9 + try: + path.relative_to(base) + except ValueError: + pass + return False + + @router_ui.get('/{rest_of_path:path}', include_in_schema=False) async def index_html(rest_of_path: str): """ @@ -40,7 +49,7 @@ async def index_html(rest_of_path: str): filename = uibase / rest_of_path # It's security relevant to check "relative_to". # Without this, Directory-traversal is possible. - if filename.is_file() and filename.is_relative_to(uibase): + if filename.is_file() and is_relative_to(filename, uibase): return FileResponse(str(filename)) index_file = uibase / 'index.html' From 4115121c249077ff123c2d15ac22df62ed375ed4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 16 Aug 2021 07:26:58 +0200 Subject: [PATCH 071/100] Fix missing return statement in is_Relative_to --- freqtrade/rpc/api_server/web_ui.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/rpc/api_server/web_ui.py b/freqtrade/rpc/api_server/web_ui.py index 9aae0afae..b04269c61 100644 --- a/freqtrade/rpc/api_server/web_ui.py +++ b/freqtrade/rpc/api_server/web_ui.py @@ -33,6 +33,7 @@ def is_relative_to(path, base) -> bool: # Helper function simulating behaviour of is_relative_to, which was only added in python 3.9 try: path.relative_to(base) + return True except ValueError: pass return False From f24a951ec5cbc5ee3804d5eddfbf692acff07aeb Mon Sep 17 00:00:00 2001 From: slowy07 Date: Mon, 16 Aug 2021 19:16:24 +0700 Subject: [PATCH 072/100] fix: typo spelling grammar --- docs/exchanges.md | 2 +- docs/strategy_analysis_example.md | 2 +- freqtrade/data/btanalysis.py | 2 +- freqtrade/data/converter.py | 4 ++-- freqtrade/exchange/exchange.py | 2 +- freqtrade/freqtradebot.py | 2 +- freqtrade/plot/plotting.py | 2 +- freqtrade/rpc/rpc.py | 2 +- tests/data/test_converter.py | 2 +- tests/data/test_dataprovider.py | 2 +- tests/data/test_history.py | 6 +++--- tests/strategy/test_interface.py | 2 +- 12 files changed, 15 insertions(+), 15 deletions(-) diff --git a/docs/exchanges.md b/docs/exchanges.md index 29b9bb533..5f54a524e 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -105,7 +105,7 @@ To use subaccounts with FTX, you need to edit the configuration and add the foll ## Kucoin -Kucoin requries a passphrase for each api key, you will therefore need to add this key into the configuration so your exchange section looks as follows: +Kucoin requires a passphrase for each api key, you will therefore need to add this key into the configuration so your exchange section looks as follows: ```json "exchange": { diff --git a/docs/strategy_analysis_example.md b/docs/strategy_analysis_example.md index 27192aa2f..dd7e07824 100644 --- a/docs/strategy_analysis_example.md +++ b/docs/strategy_analysis_example.md @@ -228,7 +228,7 @@ graph = generate_candlestick_graph(pair=pair, # Show graph inline # graph.show() -# Render graph in a seperate window +# Render graph in a separate window graph.show(renderer="browser") ``` diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index d62712cbb..7d97661c4 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -19,7 +19,7 @@ logger = logging.getLogger(__name__) BT_DATA_COLUMNS_OLD = ["pair", "profit_percent", "open_date", "close_date", "index", "trade_duration", "open_rate", "close_rate", "open_at_end", "sell_reason"] -# Mid-term format, crated by BacktestResult Named Tuple +# Mid-term format, created by BacktestResult Named Tuple BT_DATA_COLUMNS_MID = ['pair', 'profit_percent', 'open_date', 'close_date', 'trade_duration', 'open_rate', 'close_rate', 'open_at_end', 'sell_reason', 'fee_open', 'fee_close', 'amount', 'profit_abs', 'profit_ratio'] diff --git a/freqtrade/data/converter.py b/freqtrade/data/converter.py index 040f58d62..ca6464965 100644 --- a/freqtrade/data/converter.py +++ b/freqtrade/data/converter.py @@ -242,7 +242,7 @@ def convert_trades_format(config: Dict[str, Any], convert_from: str, convert_to: :param config: Config dictionary :param convert_from: Source format :param convert_to: Target format - :param erase: Erase souce data (does not apply if source and target format are identical) + :param erase: Erase source data (does not apply if source and target format are identical) """ from freqtrade.data.history.idatahandler import get_datahandler src = get_datahandler(config['datadir'], convert_from) @@ -267,7 +267,7 @@ def convert_ohlcv_format(config: Dict[str, Any], convert_from: str, convert_to: :param config: Config dictionary :param convert_from: Source format :param convert_to: Target format - :param erase: Erase souce data (does not apply if source and target format are identical) + :param erase: Erase source data (does not apply if source and target format are identical) """ from freqtrade.data.history.idatahandler import get_datahandler src = get_datahandler(config['datadir'], convert_from) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index cde643cff..9aa5b98a8 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1497,7 +1497,7 @@ class Exchange: :returns List of trade data """ if not self.exchange_has("fetchTrades"): - raise OperationalException("This exchange does not suport downloading Trades.") + raise OperationalException("This exchange does not support downloading Trades.") return asyncio.get_event_loop().run_until_complete( self._async_get_trade_history(pair=pair, since=since, diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 09aa06adf..fb15d6e5c 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -977,7 +977,7 @@ class FreqtradeBot(LoggingMixin): # 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 aquired before cancelling. + # 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 trade.stake_amount = trade.amount * trade.open_rate diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 2fbf343ce..509c03e90 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -538,7 +538,7 @@ def load_and_plot_trades(config: Dict[str, Any]): - Initializes plot-script - Get candle (OHLCV) data - Generate Dafaframes populated with indicators and signals based on configured strategy - - Load trades excecuted during the selected period + - Load trades executed during the selected period - Generate Plotly plot objects - Generate plot files :return: None diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 902975fde..0264003a5 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -776,7 +776,7 @@ class RPC: if has_content: dataframe.loc[:, '__date_ts'] = dataframe.loc[:, 'date'].view(int64) // 1000 // 1000 - # Move open to seperate column when signal for easy plotting + # Move open to separate column when signal for easy plotting if 'buy' in dataframe.columns: buy_mask = (dataframe['buy'] == 1) buy_signals = int(buy_mask.sum()) diff --git a/tests/data/test_converter.py b/tests/data/test_converter.py index 802fd4b12..6c95a9f18 100644 --- a/tests/data/test_converter.py +++ b/tests/data/test_converter.py @@ -119,7 +119,7 @@ def test_ohlcv_fill_up_missing_data2(caplog): # 3rd candle has been filled row = data2.loc[2, :] assert row['volume'] == 0 - # close shoult match close of previous candle + # close should match close of previous candle assert row['close'] == data.loc[1, 'close'] assert row['open'] == row['close'] assert row['high'] == row['close'] diff --git a/tests/data/test_dataprovider.py b/tests/data/test_dataprovider.py index e43309743..0f42068c1 100644 --- a/tests/data/test_dataprovider.py +++ b/tests/data/test_dataprovider.py @@ -66,7 +66,7 @@ def test_historic_ohlcv_dataformat(mocker, default_conf, ohlcv_history): hdf5loadmock.assert_not_called() jsonloadmock.assert_called_once() - # Swiching to dataformat hdf5 + # Switching to dataformat hdf5 hdf5loadmock.reset_mock() jsonloadmock.reset_mock() default_conf["dataformat_ohlcv"] = "hdf5" diff --git a/tests/data/test_history.py b/tests/data/test_history.py index 9cfe861ea..13d22ebb7 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -200,15 +200,15 @@ def test_load_cached_data_for_updating(mocker, testdatadir) -> None: assert start_ts == test_data[0][0] - 1000 # timeframe starts in the center of the cached data - # should return the chached data w/o the last item + # should return the cached data w/o the last item timerange = TimeRange('date', None, test_data[0][0] / 1000 + 1, 0) data, start_ts = _load_cached_data_for_updating('UNITTEST/BTC', '1m', timerange, data_handler) assert_frame_equal(data, test_data_df.iloc[:-1]) assert test_data[-2][0] <= start_ts < test_data[-1][0] - # timeframe starts after the chached data - # should return the chached data w/o the last item + # timeframe starts after the cached data + # should return the cached data w/o the last item timerange = TimeRange('date', None, test_data[-1][0] / 1000 + 100, 0) data, start_ts = _load_cached_data_for_updating('UNITTEST/BTC', '1m', timerange, data_handler) assert_frame_equal(data, test_data_df.iloc[:-1]) diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index cb4b8bd63..eea6a85d2 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -630,7 +630,7 @@ def test_strategy_safe_wrapper_error(caplog, error): assert ret caplog.clear() - # Test supressing error + # Test suppressing error ret = strategy_safe_wrapper(failing_method, message='DeadBeef', supress_error=True)() assert log_has_re(r'DeadBeef.*', caplog) From 0264d77d8670e43304b58ad1ea2d39cff6550596 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 16 Aug 2021 20:02:25 +0200 Subject: [PATCH 073/100] Fix test for fixed typo --- tests/exchange/test_exchange.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 9ac9f84e5..a5099a3ce 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2182,7 +2182,7 @@ def test_get_historic_trades_notsupported(default_conf, mocker, caplog, exchange pair = 'ETH/BTC' with pytest.raises(OperationalException, - match="This exchange does not suport downloading Trades."): + match="This exchange does not support downloading Trades."): exchange.get_historic_trades(pair, since=trades_history[0][0], until=trades_history[-1][0]) From 3ea4b2ba00a4d3cef42def46dc0b32a7ef7b7603 Mon Sep 17 00:00:00 2001 From: axel Date: Mon, 16 Aug 2021 15:18:57 -0400 Subject: [PATCH 074/100] add custom_price_max_distance_percent security to get_valid_price, update tests --- freqtrade/freqtradebot.py | 17 ++++++++++++++--- tests/test_freqtradebot.py | 14 +++++++++++++- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 9a1b2ab0c..13632bad1 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1401,10 +1401,21 @@ class FreqtradeBot(LoggingMixin): """ if custom_price: try: - valid_price = float(custom_price) + valid_custom_price = float(custom_price) except ValueError: - valid_price = proposed_price + valid_custom_price = proposed_price else: - valid_price = proposed_price + valid_custom_price = proposed_price + + cust_p_max_dist_pct = self.config.get('custom_price_max_distance_percent', 2.0) + min_custom_price_allowed = proposed_price - ((proposed_price * cust_p_max_dist_pct) / 100) + max_custom_price_allowed = proposed_price + ((proposed_price * cust_p_max_dist_pct) / 100) + + if valid_custom_price > max_custom_price_allowed: + valid_price = max_custom_price_allowed + elif valid_custom_price < min_custom_price_allowed: + valid_price = min_custom_price_allowed + else: + valid_price = valid_custom_price return valid_price diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index a475ced48..80bcabdb6 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -4594,19 +4594,25 @@ def test_get_valid_price(mocker, default_conf) -> None: patch_RPCManager(mocker) patch_exchange(mocker) freqtrade = FreqtradeBot(default_conf) + freqtrade.config['custom_price_max_distance_percent'] = 2.0 custom_price_string = "10" custom_price_badstring = "10abc" custom_price_float = 10.0 custom_price_int = 10 - proposed_price = 12.2 + custom_price_over_max_alwd = 11.0 + custom_price_under_min_alwd = 9.0 + proposed_price = 10.1 valid_price_from_string = freqtrade.get_valid_price(custom_price_string, proposed_price) valid_price_from_badstring = freqtrade.get_valid_price(custom_price_badstring, proposed_price) valid_price_from_int = freqtrade.get_valid_price(custom_price_int, proposed_price) valid_price_from_float = freqtrade.get_valid_price(custom_price_float, proposed_price) + valid_price_at_max_alwd = freqtrade.get_valid_price(custom_price_over_max_alwd, proposed_price) + valid_price_at_min_alwd = freqtrade.get_valid_price(custom_price_under_min_alwd, proposed_price) + assert isinstance(valid_price_from_string, float) assert isinstance(valid_price_from_badstring, float) assert isinstance(valid_price_from_int, float) @@ -4616,3 +4622,9 @@ def test_get_valid_price(mocker, default_conf) -> None: assert valid_price_from_badstring == proposed_price assert valid_price_from_int == custom_price_int assert valid_price_from_float == custom_price_float + + assert valid_price_at_max_alwd != custom_price_over_max_alwd + assert valid_price_at_max_alwd > proposed_price + + assert valid_price_at_min_alwd != custom_price_under_min_alwd + assert valid_price_at_min_alwd < proposed_price From faff40577a48acc161593e685c707664c5d9083d Mon Sep 17 00:00:00 2001 From: axel Date: Mon, 16 Aug 2021 15:33:05 -0400 Subject: [PATCH 075/100] fix test_execute_buy In case of custom entry price --- tests/test_freqtradebot.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 80bcabdb6..5b5e3ce28 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -905,13 +905,14 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order, limit_buy_order freqtrade.execute_buy(pair, stake_amount) # In case of custom entry price + mocker.patch('freqtrade.exchange.Exchange.get_rate', return_value=0.50) limit_buy_order['status'] = 'open' limit_buy_order['id'] = '5566' - freqtrade.strategy.custom_entry_price = lambda **kwargs: 0.77 + freqtrade.strategy.custom_entry_price = lambda **kwargs: 0.508 assert freqtrade.execute_buy(pair, stake_amount) trade = Trade.query.all()[6] assert trade - assert trade.open_rate_requested == 0.77 + assert trade.open_rate_requested == 0.508 # In case of custom entry price set to None limit_buy_order['status'] = 'open' From 17daba321bfbc755443c5399c9ad3569f11fc38b Mon Sep 17 00:00:00 2001 From: axel Date: Mon, 16 Aug 2021 23:09:30 -0400 Subject: [PATCH 076/100] add custom_price_max_distance_percent config option in constants --- freqtrade/constants.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index de4bc99b4..2f51f45f7 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -191,6 +191,9 @@ CONF_SCHEMA = { }, 'required': ['price_side'] }, + 'custom_price_max_distance_percent': { + 'type': 'number', 'minimum': 0.0 + }, 'order_types': { 'type': 'object', 'properties': { From f08d673a52f4034e367c26ca911911b793d6a734 Mon Sep 17 00:00:00 2001 From: axel Date: Mon, 16 Aug 2021 23:26:08 -0400 Subject: [PATCH 077/100] add details and exemple of custom_price_max_distance_percent usage in doc --- docs/strategy-advanced.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index e53f20693..babcc5e7b 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -398,7 +398,10 @@ class AwesomeStrategy(IStrategy): ``` !!! Warning - Modifying entry and exit prices will only work for limit orders. Depending on the price chosen, this can result in a lot of unfilled orders. + Modifying entry and exit prices will only work for limit orders. Depending on the price chosen, this can result in a lot of unfilled orders. By default the maximum allowed distance between the current price and the custom price is 2%, this value can be changed in config with the `custom_price_max_distance_percent` parameter. + +_Exemple_ +If the new_entryprice is 97, the proposed_rate is 100 and the `custom_price_max_distance_percent` is set to 2%, The retained valid custom entry price will be 98. !!! Warning "No backtesting support" Custom entry-prices are currently not supported during backtesting. From f47191582872279de090667cb5fc8f703125569c Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 17 Aug 2021 06:41:13 +0200 Subject: [PATCH 078/100] Add test for refresh_latest_ohlcv caching --- tests/exchange/test_exchange.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index a5099a3ce..27eeed39b 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1564,13 +1564,16 @@ def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None: pairs = [('IOTA/ETH', '5m'), ('XRP/ETH', '5m')] # empty dicts assert not exchange._klines - exchange.refresh_latest_ohlcv(pairs, cache=False) + res = exchange.refresh_latest_ohlcv(pairs, cache=False) # No caching assert not exchange._klines + + assert len(res) == len(pairs) assert exchange._api_async.fetch_ohlcv.call_count == 2 exchange._api_async.fetch_ohlcv.reset_mock() - exchange.refresh_latest_ohlcv(pairs) + res = exchange.refresh_latest_ohlcv(pairs) + assert len(res) == len(pairs) assert log_has(f'Refreshing candle (OHLCV) data for {len(pairs)} pairs', caplog) assert exchange._klines @@ -1587,12 +1590,16 @@ def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None: assert exchange.klines(pair, copy=False) is exchange.klines(pair, copy=False) # test caching - exchange.refresh_latest_ohlcv([('IOTA/ETH', '5m'), ('XRP/ETH', '5m')]) + res = exchange.refresh_latest_ohlcv([('IOTA/ETH', '5m'), ('XRP/ETH', '5m')]) + assert len(res) == len(pairs) assert exchange._api_async.fetch_ohlcv.call_count == 2 assert log_has(f"Using cached candle (OHLCV) data for pair {pairs[0][0]}, " f"timeframe {pairs[0][1]} ...", caplog) + res = exchange.refresh_latest_ohlcv([('IOTA/ETH', '5m'), ('XRP/ETH', '5m'), ('XRP/ETH', '1d')], + cache=False) + assert len(res) == 3 @pytest.mark.asyncio From 9758bed250450eb54de455987de2c2c5b93ed6be Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 17 Aug 2021 06:44:20 +0200 Subject: [PATCH 079/100] Fix refresh_latest_ohlcv bug --- freqtrade/exchange/exchange.py | 7 ++++++- freqtrade/plugins/pairlist/VolumePairList.py | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 9aa5b98a8..dbd72aca4 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1253,7 +1253,7 @@ class Exchange: logger.debug("Refreshing candle (OHLCV) data for %d pairs", len(pair_list)) input_coroutines = [] - + cached_pairs = [] # Gather coroutines to run for pair, timeframe in set(pair_list): if (((pair, timeframe) not in self._klines) @@ -1265,6 +1265,7 @@ class Exchange: "Using cached candle (OHLCV) data for pair %s, timeframe %s ...", pair, timeframe ) + cached_pairs.append((pair, timeframe)) results = asyncio.get_event_loop().run_until_complete( asyncio.gather(*input_coroutines, return_exceptions=True)) @@ -1287,6 +1288,10 @@ class Exchange: results_df[(pair, timeframe)] = ohlcv_df if cache: self._klines[(pair, timeframe)] = ohlcv_df + # Return cached klines + for pair, timeframe in cached_pairs: + results_df[(pair, timeframe)] = self.klines((pair, timeframe), copy=False) + return results_df def _now_is_time_to_refresh(self, pair: str, timeframe: str) -> bool: diff --git a/freqtrade/plugins/pairlist/VolumePairList.py b/freqtrade/plugins/pairlist/VolumePairList.py index 8d01aeee1..23291d39e 100644 --- a/freqtrade/plugins/pairlist/VolumePairList.py +++ b/freqtrade/plugins/pairlist/VolumePairList.py @@ -115,7 +115,7 @@ class VolumePairList(IPairList): pairlist = self._pair_cache.get('pairlist') if pairlist: # Item found - no refresh necessary - return pairlist + return pairlist.copy() else: # Use fresh pairlist # Check if pair quote currency equals to the stake currency. From 37e3d20357943e18e0f0197a11e911aa4d2031e0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 17 Aug 2021 06:54:10 +0200 Subject: [PATCH 080/100] Fix no-event-loop available closes #5433 --- freqtrade/rpc/api_server/uvicorn_threaded.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/api_server/uvicorn_threaded.py b/freqtrade/rpc/api_server/uvicorn_threaded.py index 2f72cb74c..b63999f51 100644 --- a/freqtrade/rpc/api_server/uvicorn_threaded.py +++ b/freqtrade/rpc/api_server/uvicorn_threaded.py @@ -32,8 +32,11 @@ class UvicornServer(uvicorn.Server): asyncio_setup() else: asyncio.set_event_loop(uvloop.new_event_loop()) - - loop = asyncio.get_event_loop() + try: + loop = asyncio.get_event_loop() + except RuntimeError: + # When running in a thread, we'll not have an eventloop yet. + loop = asyncio.new_event_loop() loop.run_until_complete(self.serve(sockets=sockets)) @contextlib.contextmanager From 4164f9385313f98bce671aaa2fc9182bd5ad72d5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 17 Aug 2021 20:41:08 +0200 Subject: [PATCH 081/100] Simplify fiat_convert and handle multi-mappings --- freqtrade/rpc/fiat_convert.py | 40 ++++++++++++++++++++-------------- tests/rpc/test_fiat_convert.py | 39 +++++++++++++++++++++------------ 2 files changed, 49 insertions(+), 30 deletions(-) diff --git a/freqtrade/rpc/fiat_convert.py b/freqtrade/rpc/fiat_convert.py index cdc09b437..f4e82261e 100644 --- a/freqtrade/rpc/fiat_convert.py +++ b/freqtrade/rpc/fiat_convert.py @@ -5,7 +5,7 @@ e.g BTC to USD import datetime import logging -from typing import Dict +from typing import Dict, List from cachetools.ttl import TTLCache from pycoingecko import CoinGeckoAPI @@ -25,8 +25,7 @@ class CryptoToFiatConverter: """ __instance = None _coingekko: CoinGeckoAPI = None - - _cryptomap: Dict = {} + _coinlistings: List[Dict] = [] _backoff: float = 0.0 def __new__(cls): @@ -49,9 +48,8 @@ class CryptoToFiatConverter: def _load_cryptomap(self) -> None: try: - coinlistings = self._coingekko.get_coins_list() - # Create mapping table from symbol to coingekko_id - self._cryptomap = {x['symbol']: x['id'] for x in coinlistings} + # Use list-comprehension to ensure we get a list. + self._coinlistings = [x for x in self._coingekko.get_coins_list()] except RequestException as request_exception: if "429" in str(request_exception): logger.warning( @@ -69,6 +67,24 @@ class CryptoToFiatConverter: logger.error( f"Could not load FIAT Cryptocurrency map for the following problem: {exception}") + def _get_gekko_id(self, crypto_symbol): + if not self._coinlistings: + if self._backoff <= datetime.datetime.now().timestamp(): + self._load_cryptomap() + # Still not loaded. + if not self._coinlistings: + return None + else: + return None + found = [x for x in self._coinlistings if x['symbol'] == crypto_symbol] + if len(found) == 1: + return found[0]['id'] + + if len(found) > 0: + # Wrong! + logger.warning(f"Found multiple mappings in goingekko for {crypto_symbol}.") + return None + def convert_amount(self, crypto_amount: float, crypto_symbol: str, fiat_symbol: str) -> float: """ Convert an amount of crypto-currency to fiat @@ -143,22 +159,14 @@ class CryptoToFiatConverter: if crypto_symbol == fiat_symbol: return 1.0 - if self._cryptomap == {}: - if self._backoff <= datetime.datetime.now().timestamp(): - self._load_cryptomap() - # return 0.0 if we still don't have data to check, no reason to proceed - if self._cryptomap == {}: - return 0.0 - else: - return 0.0 + _gekko_id = self._get_gekko_id(crypto_symbol) - if crypto_symbol not in self._cryptomap: + if not _gekko_id: # return 0 for unsupported stake currencies (fiat-convert should not break the bot) logger.warning("unsupported crypto-symbol %s - returning 0.0", crypto_symbol) return 0.0 try: - _gekko_id = self._cryptomap[crypto_symbol] return float( self._coingekko.get_price( ids=_gekko_id, diff --git a/tests/rpc/test_fiat_convert.py b/tests/rpc/test_fiat_convert.py index 9fb1122f5..2fe5d4a56 100644 --- a/tests/rpc/test_fiat_convert.py +++ b/tests/rpc/test_fiat_convert.py @@ -22,7 +22,7 @@ def test_fiat_convert_is_supported(mocker): def test_fiat_convert_find_price(mocker): fiat_convert = CryptoToFiatConverter() - fiat_convert._cryptomap = {} + fiat_convert._coinlistings = {} fiat_convert._backoff = 0 mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._load_cryptomap', return_value=None) @@ -44,7 +44,7 @@ def test_fiat_convert_find_price(mocker): def test_fiat_convert_unsupported_crypto(mocker, caplog): - mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._cryptomap', return_value=[]) + mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._coinlistings', return_value=[]) fiat_convert = CryptoToFiatConverter() assert fiat_convert._find_price(crypto_symbol='CRYPTO_123', fiat_symbol='EUR') == 0.0 assert log_has('unsupported crypto-symbol CRYPTO_123 - returning 0.0', caplog) @@ -88,9 +88,9 @@ def test_fiat_convert_two_FIAT(mocker): def test_loadcryptomap(mocker): fiat_convert = CryptoToFiatConverter() - assert len(fiat_convert._cryptomap) == 2 + assert len(fiat_convert._coinlistings) == 2 - assert fiat_convert._cryptomap["btc"] == "bitcoin" + assert fiat_convert._get_gekko_id("btc") == "bitcoin" def test_fiat_init_network_exception(mocker): @@ -102,11 +102,10 @@ def test_fiat_init_network_exception(mocker): ) # with pytest.raises(RequestEsxception): fiat_convert = CryptoToFiatConverter() - fiat_convert._cryptomap = {} + fiat_convert._coinlistings = {} fiat_convert._load_cryptomap() - length_cryptomap = len(fiat_convert._cryptomap) - assert length_cryptomap == 0 + assert len(fiat_convert._coinlistings) == 0 def test_fiat_convert_without_network(mocker): @@ -132,11 +131,10 @@ def test_fiat_too_many_requests_response(mocker, caplog): ) # with pytest.raises(RequestEsxception): fiat_convert = CryptoToFiatConverter() - fiat_convert._cryptomap = {} + fiat_convert._coinlistings = {} fiat_convert._load_cryptomap() - length_cryptomap = len(fiat_convert._cryptomap) - assert length_cryptomap == 0 + assert len(fiat_convert._coinlistings) == 0 assert fiat_convert._backoff > datetime.datetime.now().timestamp() assert log_has( 'Too many requests for Coingecko API, backing off and trying again later.', @@ -144,20 +142,33 @@ def test_fiat_too_many_requests_response(mocker, caplog): ) +def test_fiat_multiple_coins(mocker, caplog): + fiat_convert = CryptoToFiatConverter() + fiat_convert._coinlistings = [ + {'id': 'helium', 'symbol': 'hnt', 'name': 'Helium'}, + {'id': 'hymnode', 'symbol': 'hnt', 'name': 'Hymnode'}, + {'id': 'bitcoin', 'symbol': 'btc', 'name': 'Bitcoin'}, + ] + + assert fiat_convert._get_gekko_id('btc') == 'bitcoin' + assert fiat_convert._get_gekko_id('hnt') is None + + assert log_has('Found multiple mappings in goingekko for hnt.', caplog) + + def test_fiat_invalid_response(mocker, caplog): # Because CryptoToFiatConverter is a Singleton we reset the listings - listmock = MagicMock(return_value="{'novalidjson':DEADBEEFf}") + listmock = MagicMock(return_value=None) mocker.patch.multiple( 'freqtrade.rpc.fiat_convert.CoinGeckoAPI', get_coins_list=listmock, ) # with pytest.raises(RequestEsxception): fiat_convert = CryptoToFiatConverter() - fiat_convert._cryptomap = {} + fiat_convert._coinlistings = [] fiat_convert._load_cryptomap() - length_cryptomap = len(fiat_convert._cryptomap) - assert length_cryptomap == 0 + assert len(fiat_convert._coinlistings) == 0 assert log_has_re('Could not load FIAT Cryptocurrency map for the following problem: .*', caplog) From 6e41add40e3d223f26d9acd9054571e1b7e947b7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 18 Aug 2021 06:25:30 +0200 Subject: [PATCH 082/100] Version bump ccxt closes #5437 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 3bd34fad8..0e00c289a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.21.2 pandas==1.3.2 -ccxt==1.55.6 +ccxt==1.55.13 # Pin cryptography for now due to rust build errors with piwheels cryptography==3.4.7 aiohttp==3.7.4.post0 From acb00cd0728efe1e2bee29fa3ed2ae2b7173935b Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 18 Aug 2021 06:25:52 +0200 Subject: [PATCH 083/100] Use realistic threshold for "get_fee" test --- tests/exchange/test_ccxt_compat.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index dce10da84..ffca9ec81 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -142,8 +142,8 @@ class TestCCXTExchange(): def test_ccxt_get_fee(self, exchange): exchange, exchangename = exchange pair = EXCHANGES[exchangename]['pair'] - - assert 0 < exchange.get_fee(pair, 'limit', 'buy') < 1 - assert 0 < exchange.get_fee(pair, 'limit', 'sell') < 1 - assert 0 < exchange.get_fee(pair, 'market', 'buy') < 1 - assert 0 < exchange.get_fee(pair, 'market', 'sell') < 1 + threshold = 0.01 + assert 0 < exchange.get_fee(pair, 'limit', 'buy') < threshold + assert 0 < exchange.get_fee(pair, 'limit', 'sell') < threshold + assert 0 < exchange.get_fee(pair, 'market', 'buy') < threshold + assert 0 < exchange.get_fee(pair, 'market', 'sell') < threshold From 2fb9f6e2f4d4621a9f87c27ee22588344901e29e Mon Sep 17 00:00:00 2001 From: axel Date: Wed, 18 Aug 2021 05:07:37 -0400 Subject: [PATCH 084/100] rename custom price max distance option in config, update formula and test associated --- docs/strategy-advanced.md | 4 ++-- freqtrade/constants.py | 2 +- freqtrade/freqtradebot.py | 6 +++--- tests/test_freqtradebot.py | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index babcc5e7b..a0ae7201f 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -398,10 +398,10 @@ class AwesomeStrategy(IStrategy): ``` !!! Warning - Modifying entry and exit prices will only work for limit orders. Depending on the price chosen, this can result in a lot of unfilled orders. By default the maximum allowed distance between the current price and the custom price is 2%, this value can be changed in config with the `custom_price_max_distance_percent` parameter. + Modifying entry and exit prices will only work for limit orders. Depending on the price chosen, this can result in a lot of unfilled orders. By default the maximum allowed distance between the current price and the custom price is 2%, this value can be changed in config with the `custom_price_max_distance_ratio` parameter. _Exemple_ -If the new_entryprice is 97, the proposed_rate is 100 and the `custom_price_max_distance_percent` is set to 2%, The retained valid custom entry price will be 98. +If the new_entryprice is 97, the proposed_rate is 100 and the `custom_price_max_distance_ratio` is set to 2%, The retained valid custom entry price will be 98. !!! Warning "No backtesting support" Custom entry-prices are currently not supported during backtesting. diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 2f51f45f7..cde276ac0 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -191,7 +191,7 @@ CONF_SCHEMA = { }, 'required': ['price_side'] }, - 'custom_price_max_distance_percent': { + 'custom_price_max_distance_ratio': { 'type': 'number', 'minimum': 0.0 }, 'order_types': { diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index bd62934c5..caf201451 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1407,9 +1407,9 @@ class FreqtradeBot(LoggingMixin): else: valid_custom_price = proposed_price - cust_p_max_dist_pct = self.config.get('custom_price_max_distance_percent', 2.0) - min_custom_price_allowed = proposed_price - ((proposed_price * cust_p_max_dist_pct) / 100) - max_custom_price_allowed = proposed_price + ((proposed_price * cust_p_max_dist_pct) / 100) + cust_p_max_dist_r = self.config.get('custom_price_max_distance_ratio', 0.02) + min_custom_price_allowed = proposed_price - (proposed_price * cust_p_max_dist_r) + max_custom_price_allowed = proposed_price + (proposed_price * cust_p_max_dist_r) if valid_custom_price > max_custom_price_allowed: valid_price = max_custom_price_allowed diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 5b5e3ce28..21bad5c64 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -4595,7 +4595,7 @@ def test_get_valid_price(mocker, default_conf) -> None: patch_RPCManager(mocker) patch_exchange(mocker) freqtrade = FreqtradeBot(default_conf) - freqtrade.config['custom_price_max_distance_percent'] = 2.0 + freqtrade.config['custom_price_max_distance_ratio'] = 0.02 custom_price_string = "10" custom_price_badstring = "10abc" From 9469c6dfa9bc78bd6c9ff6ae17f0156ada4d6b39 Mon Sep 17 00:00:00 2001 From: axel Date: Wed, 18 Aug 2021 05:10:29 -0400 Subject: [PATCH 085/100] small cosmetic changes in doc related to custom entry and exit exemple --- docs/strategy-advanced.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index a0ae7201f..f5f2d9197 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -400,8 +400,8 @@ class AwesomeStrategy(IStrategy): !!! Warning Modifying entry and exit prices will only work for limit orders. Depending on the price chosen, this can result in a lot of unfilled orders. By default the maximum allowed distance between the current price and the custom price is 2%, this value can be changed in config with the `custom_price_max_distance_ratio` parameter. -_Exemple_ -If the new_entryprice is 97, the proposed_rate is 100 and the `custom_price_max_distance_ratio` is set to 2%, The retained valid custom entry price will be 98. +!!! Exemple + If the new_entryprice is 97, the proposed_rate is 100 and the `custom_price_max_distance_ratio` is set to 2%, The retained valid custom entry price will be 98. !!! Warning "No backtesting support" Custom entry-prices are currently not supported during backtesting. From ffd60f392be7a83cf2bf786314322eba4e27bacf Mon Sep 17 00:00:00 2001 From: axel Date: Wed, 18 Aug 2021 05:22:45 -0400 Subject: [PATCH 086/100] add custom price max distance ratio option in configuration.md --- docs/configuration.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/configuration.md b/docs/configuration.md index fab3004a5..09198e019 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -110,6 +110,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `ignore_buying_expired_candle_after` | Specifies the number of seconds until a buy signal is no longer used.
**Datatype:** Integer | `order_types` | Configure order-types depending on the action (`"buy"`, `"sell"`, `"stoploss"`, `"stoploss_on_exchange"`). [More information below](#understand-order_types). [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Dict | `order_time_in_force` | Configure time in force for buy and sell orders. [More information below](#understand-order_time_in_force). [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Dict +| `custom_price_max_distance_ratio` | Configure maximum distance ratio between current and custom entry or exit price.
*Defaults to `0.02` 2%).*
**Datatype:** Positive float | `exchange.name` | **Required.** Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename).
**Datatype:** String | `exchange.sandbox` | Use the 'sandbox' version of the exchange, where the exchange provides a sandbox for risk-free integration. See [here](sandbox-testing.md) in more details.
**Datatype:** Boolean | `exchange.key` | API key to use for the exchange. Only required when you are in production mode.
**Keep it in secret, do not disclose publicly.**
**Datatype:** String From d97fc1e484ceb29a93f31e259556e9f74abda717 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 18 Aug 2021 19:55:11 +0200 Subject: [PATCH 087/100] Update docs/strategy-advanced.md --- docs/strategy-advanced.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index f5f2d9197..b039f542f 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -400,7 +400,7 @@ class AwesomeStrategy(IStrategy): !!! Warning Modifying entry and exit prices will only work for limit orders. Depending on the price chosen, this can result in a lot of unfilled orders. By default the maximum allowed distance between the current price and the custom price is 2%, this value can be changed in config with the `custom_price_max_distance_ratio` parameter. -!!! Exemple +!!! Example If the new_entryprice is 97, the proposed_rate is 100 and the `custom_price_max_distance_ratio` is set to 2%, The retained valid custom entry price will be 98. !!! Warning "No backtesting support" From 9951f510795c241f516d6046b12699be885e84b6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 18 Aug 2021 20:20:11 +0200 Subject: [PATCH 088/100] Update test to ensure direction of movement is correct --- freqtrade/freqtradebot.py | 12 ++++-------- tests/test_freqtradebot.py | 4 ++-- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index caf201451..e7a2a3784 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1411,11 +1411,7 @@ class FreqtradeBot(LoggingMixin): min_custom_price_allowed = proposed_price - (proposed_price * cust_p_max_dist_r) max_custom_price_allowed = proposed_price + (proposed_price * cust_p_max_dist_r) - if valid_custom_price > max_custom_price_allowed: - valid_price = max_custom_price_allowed - elif valid_custom_price < min_custom_price_allowed: - valid_price = min_custom_price_allowed - else: - valid_price = valid_custom_price - - return valid_price + # Bracket between min_custom_price_allowed and max_custom_price_allowed + return max( + min(valid_custom_price, max_custom_price_allowed), + min_custom_price_allowed) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 21bad5c64..a2bb01a4b 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -4624,8 +4624,8 @@ def test_get_valid_price(mocker, default_conf) -> None: assert valid_price_from_int == custom_price_int assert valid_price_from_float == custom_price_float - assert valid_price_at_max_alwd != custom_price_over_max_alwd + assert valid_price_at_max_alwd < custom_price_over_max_alwd assert valid_price_at_max_alwd > proposed_price - assert valid_price_at_min_alwd != custom_price_under_min_alwd + assert valid_price_at_min_alwd > custom_price_under_min_alwd assert valid_price_at_min_alwd < proposed_price From dc0b4d07d48b78541701c5939ada74c31304c200 Mon Sep 17 00:00:00 2001 From: topscoder <86197446+topscoder@users.noreply.github.com> Date: Wed, 18 Aug 2021 20:52:11 +0200 Subject: [PATCH 089/100] Fix typo Reseting -> Resetting --- setup.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.sh b/setup.sh index a85bd3104..feb0241f8 100755 --- a/setup.sh +++ b/setup.sh @@ -163,7 +163,7 @@ function update() { # Reset Develop or Stable branch function reset() { echo "----------------------------" - echo "Reseting branch and virtual env" + echo "Resetting branch and virtual env" echo "----------------------------" if [ "1" == $(git branch -vv |grep -cE "\* develop|\* stable") ] From ba5abb20bd07211022b0db1a4cf47fa5ec7e9609 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 20 Aug 2021 06:30:27 +0200 Subject: [PATCH 090/100] Run compat ci for gate.io --- requirements.txt | 2 +- tests/exchange/test_ccxt_compat.py | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 0e00c289a..09109baaf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.21.2 pandas==1.3.2 -ccxt==1.55.13 +ccxt==1.55.20 # Pin cryptography for now due to rust build errors with piwheels cryptography==3.4.7 aiohttp==3.7.4.post0 diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index ffca9ec81..3a32d108b 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -42,6 +42,11 @@ EXCHANGES = { 'hasQuoteVolume': True, 'timeframe': '5m', }, + 'gateio': { + 'pair': 'BTC/USDT', + 'hasQuoteVolume': True, + 'timeframe': '5m', + }, } From 695a1e21bf0b622bffc78dfd56073367cd713cc7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 20 Aug 2021 06:51:04 +0200 Subject: [PATCH 091/100] Set gate.io download limit to 1000 candles --- freqtrade/exchange/__init__.py | 1 + freqtrade/exchange/gateio.py | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 freqtrade/exchange/gateio.py diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 015e0c869..b0c88a51a 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -15,6 +15,7 @@ from freqtrade.exchange.exchange import (available_exchanges, ccxt_exchanges, timeframe_to_seconds, validate_exchange, validate_exchanges) from freqtrade.exchange.ftx import Ftx +from freqtrade.exchange.gateio import Gateio from freqtrade.exchange.hitbtc import Hitbtc from freqtrade.exchange.kraken import Kraken from freqtrade.exchange.kucoin import Kucoin diff --git a/freqtrade/exchange/gateio.py b/freqtrade/exchange/gateio.py new file mode 100644 index 000000000..9c910a10d --- /dev/null +++ b/freqtrade/exchange/gateio.py @@ -0,0 +1,23 @@ +""" Gate.io exchange subclass """ +import logging +from typing import Dict + +from freqtrade.exchange import Exchange + + +logger = logging.getLogger(__name__) + + +class Gateio(Exchange): + """ + Gate.io exchange class. Contains adjustments needed for Freqtrade to work + with this exchange. + + Please note that this exchange is not included in the list of exchanges + officially supported by the Freqtrade development team. So some features + may still not work as expected. + """ + + _ft_has: Dict = { + "ohlcv_candle_limit": 1000, + } From 40ad4510194e3eba6512fa1f2ca7d071ae38babd Mon Sep 17 00:00:00 2001 From: Masoud Azizi Date: Thu, 19 Aug 2021 18:34:02 +0000 Subject: [PATCH 092/100] Download-data log process added Download-data log process added pytest assert solved --- freqtrade/data/history/history_utils.py | 33 ++++++++++++++----------- tests/data/test_history.py | 10 +++++--- 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/freqtrade/data/history/history_utils.py b/freqtrade/data/history/history_utils.py index 1459dfd78..6f125aaa9 100644 --- a/freqtrade/data/history/history_utils.py +++ b/freqtrade/data/history/history_utils.py @@ -117,10 +117,11 @@ def refresh_data(datadir: Path, :param timerange: Limit data to be loaded to this timerange """ data_handler = get_datahandler(datadir, data_format) - for pair in pairs: - _download_pair_history(pair=pair, timeframe=timeframe, - datadir=datadir, timerange=timerange, - exchange=exchange, data_handler=data_handler) + for idx, pair in enumerate(pairs): + process = f'{idx}/{len(pairs)}' + _download_pair_history(pair=pair, process=process, + timeframe=timeframe, datadir=datadir, + timerange=timerange, exchange=exchange, data_handler=data_handler) def _load_cached_data_for_updating(pair: str, timeframe: str, timerange: Optional[TimeRange], @@ -153,13 +154,14 @@ def _load_cached_data_for_updating(pair: str, timeframe: str, timerange: Optiona return data, start_ms -def _download_pair_history(datadir: Path, +def _download_pair_history(pair: str, *, + datadir: Path, exchange: Exchange, - pair: str, *, - new_pairs_days: int = 30, timeframe: str = '5m', - timerange: Optional[TimeRange] = None, - data_handler: IDataHandler = None) -> bool: + process: str = '', + new_pairs_days: int = 30, + data_handler: IDataHandler = None, + timerange: Optional[TimeRange] = None) -> bool: """ Download latest candles from the exchange for the pair and timeframe passed in parameters The data is downloaded starting from the last correct data that @@ -177,7 +179,7 @@ def _download_pair_history(datadir: Path, try: logger.info( - f'Download history data for pair: "{pair}", timeframe: {timeframe} ' + f'Download history data for pair: "{pair}" ({process}), timeframe: {timeframe} ' f'and store in {datadir}.' ) @@ -234,7 +236,7 @@ def refresh_backtest_ohlcv_data(exchange: Exchange, pairs: List[str], timeframes """ pairs_not_available = [] data_handler = get_datahandler(datadir, data_format) - for pair in pairs: + for idx, pair in enumerate(pairs, start=1): if pair not in exchange.markets: pairs_not_available.append(pair) logger.info(f"Skipping pair {pair}...") @@ -247,10 +249,11 @@ def refresh_backtest_ohlcv_data(exchange: Exchange, pairs: List[str], timeframes f'Deleting existing data for pair {pair}, interval {timeframe}.') logger.info(f'Downloading pair {pair}, interval {timeframe}.') - _download_pair_history(datadir=datadir, exchange=exchange, - pair=pair, timeframe=str(timeframe), - new_pairs_days=new_pairs_days, - timerange=timerange, data_handler=data_handler) + process = f'{idx}/{len(pairs)}' + _download_pair_history(pair=pair, process=process, + datadir=datadir, exchange=exchange, + timerange=timerange, data_handler=data_handler, + timeframe=str(timeframe), new_pairs_days=new_pairs_days) return pairs_not_available diff --git a/tests/data/test_history.py b/tests/data/test_history.py index 13d22ebb7..bcc2bf052 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -133,8 +133,8 @@ def test_load_data_with_new_pair_1min(ohlcv_history_list, mocker, caplog, load_pair_history(datadir=tmpdir1, timeframe='1m', pair='MEME/BTC') assert file.is_file() assert log_has_re( - 'Download history data for pair: "MEME/BTC", timeframe: 1m ' - 'and store in .*', caplog + 'Download history data for pair: "MEME/BTC" (0/1), timeframe: 1m ' + 'and store in', caplog ) @@ -278,8 +278,10 @@ def test_download_pair_history2(mocker, default_conf, testdatadir) -> None: return_value=None) mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=tick) exchange = get_patched_exchange(mocker, default_conf) - _download_pair_history(testdatadir, exchange, pair="UNITTEST/BTC", timeframe='1m') - _download_pair_history(testdatadir, exchange, pair="UNITTEST/BTC", timeframe='3m') + _download_pair_history(datadir=testdatadir, exchange=exchange, pair="UNITTEST/BTC", + timeframe='1m') + _download_pair_history(datadir=testdatadir, exchange=exchange, pair="UNITTEST/BTC", + timeframe='3m') assert json_dump_mock.call_count == 2 From 127f470bc3928b052fc5ac00d85f4422ae4e5f1f Mon Sep 17 00:00:00 2001 From: Masoud Azizi Date: Fri, 20 Aug 2021 20:28:10 +0000 Subject: [PATCH 093/100] .* ADDED --- tests/data/test_history.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/data/test_history.py b/tests/data/test_history.py index bcc2bf052..e9d2c3638 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -133,8 +133,8 @@ def test_load_data_with_new_pair_1min(ohlcv_history_list, mocker, caplog, load_pair_history(datadir=tmpdir1, timeframe='1m', pair='MEME/BTC') assert file.is_file() assert log_has_re( - 'Download history data for pair: "MEME/BTC" (0/1), timeframe: 1m ' - 'and store in', caplog + r'Download history data for pair: "MEME/BTC" \(0/1\), timeframe: 1m ' + r'and store in .*', caplog ) From 56759cea7b8697f3e2f687e8e2f47bdbf001c593 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 22 Aug 2021 14:37:45 +0200 Subject: [PATCH 094/100] Add "high fee rate" blocker to fees from trades part of #5415 (potentially fixing this) --- freqtrade/freqtradebot.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index e7a2a3784..41d61f946 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1381,7 +1381,9 @@ class FreqtradeBot(LoggingMixin): if fee_currency: # fee_rate should use mean fee_rate = sum(fee_rate_array) / float(len(fee_rate_array)) if fee_rate_array else None - trade.update_fee(fee_cost, fee_currency, fee_rate, order.get('side', '')) + if fee_rate is not None and fee_rate < 0.02: + # Only update if fee-rate is < 2% + trade.update_fee(fee_cost, fee_currency, fee_rate, order.get('side', '')) if not isclose(amount, order_amount, abs_tol=constants.MATH_CLOSE_PREC): logger.warning(f"Amount {amount} does not match amount {trade.amount}") From 04b4deab588897d258ec8f44851875f86e99b3fa Mon Sep 17 00:00:00 2001 From: JoeSchr Date: Sun, 22 Aug 2021 20:14:36 +0300 Subject: [PATCH 095/100] Update strategy-advanced.md Add `` for consistency --- docs/strategy-advanced.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index b039f542f..4409af6ea 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -361,7 +361,7 @@ See [Dataframe access](#dataframe-access) for more information about dataframe u By default, freqtrade use the orderbook to automatically set an order price([Relevant documentation](configuration.md#prices-used-for-orders)), you also have the option to create custom order prices based on your strategy. -You can use this feature by creating a `custom_entry_price()` function in your strategy file to customize entry prices and custom_exit_price for exits. +You can use this feature by creating a `custom_entry_price()` function in your strategy file to customize entry prices and `custom_exit_price()` for exits. !!! Note If your custom pricing function return None or an invalid value, price will fall back to `proposed_rate`, which is based on the regular pricing configuration. From 3026583ed480cdaab0be78554fcc0d68cf85ed00 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 22 Aug 2021 19:40:09 +0200 Subject: [PATCH 096/100] Reduce verbosity of "is in blacklist" logging --- freqtrade/plugins/pairlist/VolumePairList.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/plugins/pairlist/VolumePairList.py b/freqtrade/plugins/pairlist/VolumePairList.py index 23291d39e..c70e4a904 100644 --- a/freqtrade/plugins/pairlist/VolumePairList.py +++ b/freqtrade/plugins/pairlist/VolumePairList.py @@ -4,6 +4,7 @@ Volume PairList provider Provides dynamic pair list based on trade volumes """ import logging +from functools import partial from typing import Any, Dict, List import arrow @@ -203,7 +204,7 @@ class VolumePairList(IPairList): # 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, logger.info) + pairs = self.verify_blacklist(pairs, partial(self.log_once, logmethod=logger.info)) # Limit pairlist to the requested number of pairs pairs = pairs[:self._number_pairs] From 4ee467f8575ae2d2f3488bad00ba4ee774b4e8bb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Aug 2021 03:01:26 +0000 Subject: [PATCH 097/100] Bump ccxt from 1.55.20 to 1.55.28 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.55.20 to 1.55.28. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/exchanges.cfg) - [Commits](https://github.com/ccxt/ccxt/compare/1.55.20...1.55.28) --- updated-dependencies: - dependency-name: ccxt dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 09109baaf..f7934c903 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.21.2 pandas==1.3.2 -ccxt==1.55.20 +ccxt==1.55.28 # Pin cryptography for now due to rust build errors with piwheels cryptography==3.4.7 aiohttp==3.7.4.post0 From 0f9bfcf8b0699b9935d96d7bf18e3091693d0245 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Aug 2021 03:01:28 +0000 Subject: [PATCH 098/100] Bump types-cachetools from 0.1.10 to 4.2.0 Bumps [types-cachetools](https://github.com/python/typeshed) from 0.1.10 to 4.2.0. - [Release notes](https://github.com/python/typeshed/releases) - [Commits](https://github.com/python/typeshed/commits) --- updated-dependencies: - dependency-name: types-cachetools dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index b20cb6693..67ee0035b 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -19,7 +19,7 @@ isort==5.9.3 nbconvert==6.1.0 # mypy types -types-cachetools==0.1.10 +types-cachetools==4.2.0 types-filelock==0.1.5 types-requests==2.25.6 types-tabulate==0.8.2 From 90f1845eafa03f9a6236904bc78a75190bad0065 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Aug 2021 03:01:33 +0000 Subject: [PATCH 099/100] Bump prompt-toolkit from 3.0.19 to 3.0.20 Bumps [prompt-toolkit](https://github.com/prompt-toolkit/python-prompt-toolkit) from 3.0.19 to 3.0.20. - [Release notes](https://github.com/prompt-toolkit/python-prompt-toolkit/releases) - [Changelog](https://github.com/prompt-toolkit/python-prompt-toolkit/blob/master/CHANGELOG) - [Commits](https://github.com/prompt-toolkit/python-prompt-toolkit/compare/3.0.19...3.0.20) --- updated-dependencies: - dependency-name: prompt-toolkit dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 09109baaf..11b48cbc3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -40,4 +40,4 @@ aiofiles==0.7.0 colorama==0.4.4 # Building config files interactively questionary==1.10.0 -prompt-toolkit==3.0.19 +prompt-toolkit==3.0.20 From 345c7ab64b8a7422bdc555605c7c1cb1669c61d6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Aug 2021 03:01:42 +0000 Subject: [PATCH 100/100] Bump sqlalchemy from 1.4.22 to 1.4.23 Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 1.4.22 to 1.4.23. - [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases) - [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/master/CHANGES) - [Commits](https://github.com/sqlalchemy/sqlalchemy/commits) --- updated-dependencies: - dependency-name: sqlalchemy dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 09109baaf..1a46fc828 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ ccxt==1.55.20 # Pin cryptography for now due to rust build errors with piwheels cryptography==3.4.7 aiohttp==3.7.4.post0 -SQLAlchemy==1.4.22 +SQLAlchemy==1.4.23 python-telegram-bot==13.7 arrow==1.1.1 cachetools==4.2.2