From f11f5d17e91e05e2f2ac7729ccf0b4f29a352bd6 Mon Sep 17 00:00:00 2001 From: axel Date: Sat, 31 Jul 2021 00:05:45 -0400 Subject: [PATCH 01/34] 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 02/34] 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 03/34] 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 04/34] 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 05/34] 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 06/34] 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 07/34] 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 08/34] 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 09/34] 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 10/34] 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 11/34] 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 12/34] 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 13/34] 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 14/34] 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 15/34] 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 16/34] 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 17/34] 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 18/34] 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 19/34] 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 ae11be39706b37eb6733b12cee814cdf351b53d9 Mon Sep 17 00:00:00 2001 From: axel Date: Thu, 12 Aug 2021 14:47:01 -0400 Subject: [PATCH 20/34] 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 21/34] 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 22/34] 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 23/34] 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 24/34] 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 0f7ddabec80428e80f036618e65d4e72d7605af7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 14 Aug 2021 09:05:03 +0200 Subject: [PATCH 25/34] 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 3ea4b2ba00a4d3cef42def46dc0b32a7ef7b7603 Mon Sep 17 00:00:00 2001 From: axel Date: Mon, 16 Aug 2021 15:18:57 -0400 Subject: [PATCH 26/34] 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 27/34] 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 28/34] 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 29/34] 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 2fb9f6e2f4d4621a9f87c27ee22588344901e29e Mon Sep 17 00:00:00 2001 From: axel Date: Wed, 18 Aug 2021 05:07:37 -0400 Subject: [PATCH 30/34] 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 31/34] 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 32/34] 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 33/34] 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 34/34] 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