From f70166270d65589266b3ed2154d3e85136bf730d Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 28 Mar 2022 07:03:10 +0200 Subject: [PATCH] Update pricing to use entry/exit pricing --- freqtrade/configuration/config_validation.py | 10 ++--- freqtrade/exchange/exchange.py | 40 ++++++++++++++------ freqtrade/freqtradebot.py | 30 +++++++++------ freqtrade/rpc/rpc.py | 8 ++-- 4 files changed, 56 insertions(+), 32 deletions(-) diff --git a/freqtrade/configuration/config_validation.py b/freqtrade/configuration/config_validation.py index 2593dd39e..26a0c0193 100644 --- a/freqtrade/configuration/config_validation.py +++ b/freqtrade/configuration/config_validation.py @@ -193,13 +193,13 @@ def _validate_protections(conf: Dict[str, Any]) -> None: def _validate_ask_orderbook(conf: Dict[str, Any]) -> None: - ask_strategy = conf.get('ask_strategy', {}) + ask_strategy = conf.get('exit_pricing', {}) ob_min = ask_strategy.get('order_book_min') ob_max = ask_strategy.get('order_book_max') if ob_min is not None and ob_max is not None and ask_strategy.get('use_order_book'): if ob_min != ob_max: raise OperationalException( - "Using order_book_max != order_book_min in ask_strategy is no longer supported." + "Using order_book_max != order_book_min in exit_pricing is no longer supported." "Please pick one value and use `order_book_top` in the future." ) else: @@ -208,7 +208,7 @@ def _validate_ask_orderbook(conf: Dict[str, Any]) -> None: logger.warning( "DEPRECATED: " "Please use `order_book_top` instead of `order_book_min` and `order_book_max` " - "for your `ask_strategy` configuration." + "for your `exit_pricing` configuration." ) @@ -295,10 +295,10 @@ def _validate_pricing_rules(conf: Dict[str, Any]) -> None: "Please migrate your settings to use 'entry_pricing' and 'exit_pricing'." ) conf['entry_pricing'] = {} - for obj in list(conf.get('bid_strategy').keys()): + for obj in list(conf.get('bid_strategy', {}).keys()): process_deprecated_setting(conf, 'bid_strategy', obj, 'entry_pricing', obj) del conf['bid_strategy'] conf['exit_pricing'] = {} - for obj in list(conf.get('ask_strategy').keys()): + for obj in list(conf.get('ask_strategy', {}).keys()): process_deprecated_setting(conf, 'ask_strategy', obj, 'exit_pricing', obj) del conf['ask_strategy'] diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 638c70bef..9610c546d 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -111,8 +111,8 @@ class Exchange: # Cache values for 1800 to avoid frequent polling of the exchange for prices # Caching only applies to RPC methods, so prices for open trades are still # refreshed once every iteration. - self._sell_rate_cache: TTLCache = TTLCache(maxsize=100, ttl=1800) - self._buy_rate_cache: TTLCache = TTLCache(maxsize=100, ttl=1800) + self._exit_rate_cache: TTLCache = TTLCache(maxsize=100, ttl=1800) + self._entry_rate_cache: TTLCache = TTLCache(maxsize=100, ttl=1800) # Holds candles self._klines: Dict[PairWithTimeframe, DataFrame] = {} @@ -1438,7 +1438,8 @@ class Exchange: except ccxt.BaseError as e: raise OperationalException(e) from e - def get_rate(self, pair: str, refresh: bool, side: str) -> float: + def get_rate(self, pair: str, refresh: bool, + side: Literal['entry', 'exit'], is_short: bool) -> float: """ Calculates bid/ask target bid rate - between current ask price and last price @@ -1450,9 +1451,10 @@ class Exchange: :return: float: Price :raises PricingError if orderbook price could not be determined. """ - cache_rate: TTLCache = self._buy_rate_cache if side == "buy" else self._sell_rate_cache - [strat_name, name] = ['bid_strategy', 'Buy'] if side == "buy" else ['ask_strategy', 'Sell'] + name = side.capitalize() + strat_name = 'entry_pricing' if side == "entry" else 'exit_pricing' + cache_rate: TTLCache = self._entry_rate_cache if side == "entry" else self._exit_rate_cache if not refresh: rate = cache_rate.get(pair) # Check if cache has been invalidated @@ -1462,27 +1464,43 @@ class Exchange: conf_strategy = self._config.get(strat_name, {}) - if conf_strategy.get('use_order_book', False): + price_side = conf_strategy['price_side'] + + if price_side in ('same', 'other'): + price_map = { + ('enter', 'long', 'same'): 'bid', + ('enter', 'long', 'other'): 'ask', + ('enter', 'short', 'same'): 'ask', + ('enter', 'short', 'other'): 'bid', + ('exit', 'long', 'same'): 'ask', + ('exit', 'long', 'other'): 'bid', + ('exit', 'short', 'same'): 'bid', + ('exit', 'short', 'other'): 'ask', + } + price_side = price_map[(side, 'short' if is_short else 'long', price_side)] + + price_side_word = price_side.capitalize() + + if conf_strategy.get('use_order_book', True): order_book_top = conf_strategy.get('order_book_top', 1) order_book = self.fetch_l2_order_book(pair, order_book_top) logger.debug('order_book %s', order_book) # top 1 = index 0 try: - rate = order_book[f"{conf_strategy['price_side']}s"][order_book_top - 1][0] + rate = order_book[f"{price_side}s"][order_book_top - 1][0] except (IndexError, KeyError) as e: logger.warning( f"{name} Price at location {order_book_top} from orderbook could not be " f"determined. Orderbook: {order_book}" ) raise PricingError from e - price_side = {conf_strategy['price_side'].capitalize()} - logger.debug(f"{name} price from orderbook {price_side}" + logger.debug(f"{name} price from orderbook {price_side_word}" f"side - top {order_book_top} order book {side} rate {rate:.8f}") else: - logger.debug(f"Using Last {conf_strategy['price_side'].capitalize()} / Last Price") + logger.debug(f"Using Last {price_side_word} / Last Price") ticker = self.fetch_ticker(pair) - ticker_rate = ticker[conf_strategy['price_side']] + ticker_rate = ticker[price_side] if ticker['last'] and ticker_rate: if side == 'buy' and ticker_rate > ticker['last']: balance = conf_strategy.get('ask_last_balance', 0.0) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index c70889fe8..48e457f1e 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -7,7 +7,7 @@ import traceback from datetime import datetime, time, timezone from math import isclose from threading import Lock -from typing import Any, Dict, List, Optional, Tuple +from typing import Any, Dict, List, Literal, Optional, Tuple from schedule import Scheduler @@ -511,7 +511,8 @@ class FreqtradeBot(LoggingMixin): return else: logger.debug("Max adjustment entries is set to unlimited.") - current_rate = self.exchange.get_rate(trade.pair, refresh=True, side=trade.enter_side) + current_rate = self.exchange.get_rate( + trade.pair, side='entry', is_short=trade.is_short, refresh=True) current_profit = trade.calc_profit_ratio(current_rate) min_stake_amount = self.exchange.get_min_pair_stake_amount(trade.pair, @@ -589,11 +590,11 @@ class FreqtradeBot(LoggingMixin): time_in_force = self.strategy.order_time_in_force['entry'] [side, name] = ['sell', 'Short'] if is_short else ['buy', 'Long'] - trade_side = 'short' if is_short else 'long' + trade_side: Literal['long', 'short'] = 'short' if is_short else 'long' pos_adjust = trade is not None enter_limit_requested, stake_amount, leverage = self.get_valid_enter_price_and_stake( - pair, price, stake_amount, side, trade_side, enter_tag, trade) + pair, price, stake_amount, trade_side, enter_tag, trade) if not stake_amount: return False @@ -745,7 +746,7 @@ class FreqtradeBot(LoggingMixin): def get_valid_enter_price_and_stake( self, pair: str, price: Optional[float], stake_amount: float, - side: str, trade_side: str, + trade_side: Literal['long', 'short'], entry_tag: Optional[str], trade: Optional[Trade] ) -> Tuple[float, float, float]: @@ -754,7 +755,8 @@ class FreqtradeBot(LoggingMixin): enter_limit_requested = price else: # Calculate price - proposed_enter_rate = self.exchange.get_rate(pair, refresh=True, side=side) + proposed_enter_rate = self.exchange.get_rate( + pair, side='entry', is_short=(trade_side == 'short'), refresh=True) custom_entry_price = strategy_safe_wrapper(self.strategy.custom_entry_price, default_retval=proposed_enter_rate)( pair=pair, current_time=datetime.now(timezone.utc), @@ -763,7 +765,7 @@ class FreqtradeBot(LoggingMixin): enter_limit_requested = self.get_valid_price(custom_entry_price, proposed_enter_rate) if not enter_limit_requested: - raise PricingError(f'Could not determine {side} price.') + raise PricingError('Could not determine entry price.') if trade is None: max_leverage = self.exchange.get_max_leverage(pair, stake_amount) @@ -824,7 +826,8 @@ class FreqtradeBot(LoggingMixin): current_rate = trade.open_rate_requested if self.dataprovider.runmode in (RunMode.DRY_RUN, RunMode.LIVE): - current_rate = self.exchange.get_rate(trade.pair, refresh=False, side=trade.enter_side) + current_rate = self.exchange.get_rate( + trade.pair, side='entry', is_short=trade.is_short, refresh=False) msg = { 'trade_id': trade.id, @@ -853,7 +856,8 @@ class FreqtradeBot(LoggingMixin): """ Sends rpc notification when a entry order cancel occurred. """ - current_rate = self.exchange.get_rate(trade.pair, refresh=False, side=trade.enter_side) + current_rate = self.exchange.get_rate( + trade.pair, side='entry', is_short=trade.is_short, refresh=False) msg_type = RPCMessageType.SHORT_CANCEL if trade.is_short else RPCMessageType.BUY_CANCEL msg = { 'trade_id': trade.id, @@ -935,7 +939,8 @@ class FreqtradeBot(LoggingMixin): ) logger.debug('checking exit') - exit_rate = self.exchange.get_rate(trade.pair, refresh=True, side=trade.exit_side) + exit_rate = self.exchange.get_rate( + trade.pair, side='exit', is_short=trade.is_short, refresh=True) if self._check_and_execute_exit(trade, exit_rate, enter, exit_, exit_tag): return True @@ -1433,7 +1438,7 @@ class FreqtradeBot(LoggingMixin): profit_trade = trade.calc_profit(rate=profit_rate) # Use cached rates here - it was updated seconds ago. current_rate = self.exchange.get_rate( - trade.pair, refresh=False, side=trade.exit_side) if not fill else None + trade.pair, side='exit', is_short=trade.is_short, refresh=False) if not fill else None profit_ratio = trade.calc_profit_ratio(profit_rate) gain = "profit" if profit_ratio > 0 else "loss" @@ -1482,7 +1487,8 @@ class FreqtradeBot(LoggingMixin): profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested profit_trade = trade.calc_profit(rate=profit_rate) - current_rate = self.exchange.get_rate(trade.pair, refresh=False, side=trade.exit_side) + current_rate = self.exchange.get_rate( + trade.pair, side='exit', is_short=trade.is_short, refresh=False) profit_ratio = trade.calc_profit_ratio(profit_rate) gain = "profit" if profit_ratio > 0 else "loss" diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 83e5bc355..94bc513fb 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -171,7 +171,7 @@ class RPC: if trade.is_open: try: current_rate = self._freqtrade.exchange.get_rate( - trade.pair, refresh=False, side=trade.exit_side) + trade.pair, side='exit', is_short=trade.is_short, refresh=False) except (ExchangeError, PricingError): current_rate = NAN else: @@ -231,7 +231,7 @@ class RPC: # calculate profit and send message to user try: current_rate = self._freqtrade.exchange.get_rate( - trade.pair, refresh=False, side=trade.exit_side) + trade.pair, side='exit', is_short=trade.is_short, refresh=False) except (PricingError, ExchangeError): current_rate = NAN trade_profit = trade.calc_profit(current_rate) @@ -485,7 +485,7 @@ class RPC: # Get current rate try: current_rate = self._freqtrade.exchange.get_rate( - trade.pair, refresh=False, side=trade.exit_side) + trade.pair, side='exit', is_short=trade.is_short, refresh=False) except (PricingError, ExchangeError): current_rate = NAN profit_ratio = trade.calc_profit_ratio(rate=current_rate) @@ -705,7 +705,7 @@ class RPC: if not fully_canceled: # Get current rate and execute sell current_rate = self._freqtrade.exchange.get_rate( - trade.pair, refresh=False, side=trade.exit_side) + trade.pair, side='exit', is_short=trade.is_short, refresh=True) exit_check = ExitCheckTuple(exit_type=ExitType.FORCE_SELL) order_type = ordertype or self._freqtrade.strategy.order_types.get( "forceexit", self._freqtrade.strategy.order_types["exit"])