Update pricing to use entry/exit pricing

This commit is contained in:
Matthias 2022-03-28 07:03:10 +02:00
parent 9f863369bd
commit f70166270d
4 changed files with 56 additions and 32 deletions

View File

@ -193,13 +193,13 @@ def _validate_protections(conf: Dict[str, Any]) -> None:
def _validate_ask_orderbook(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_min = ask_strategy.get('order_book_min')
ob_max = ask_strategy.get('order_book_max') 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 is not None and ob_max is not None and ask_strategy.get('use_order_book'):
if ob_min != ob_max: if ob_min != ob_max:
raise OperationalException( 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." "Please pick one value and use `order_book_top` in the future."
) )
else: else:
@ -208,7 +208,7 @@ def _validate_ask_orderbook(conf: Dict[str, Any]) -> None:
logger.warning( logger.warning(
"DEPRECATED: " "DEPRECATED: "
"Please use `order_book_top` instead of `order_book_min` and `order_book_max` " "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'." "Please migrate your settings to use 'entry_pricing' and 'exit_pricing'."
) )
conf['entry_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) process_deprecated_setting(conf, 'bid_strategy', obj, 'entry_pricing', obj)
del conf['bid_strategy'] del conf['bid_strategy']
conf['exit_pricing'] = {} 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) process_deprecated_setting(conf, 'ask_strategy', obj, 'exit_pricing', obj)
del conf['ask_strategy'] del conf['ask_strategy']

View File

@ -111,8 +111,8 @@ class Exchange:
# Cache values for 1800 to avoid frequent polling of the exchange for prices # 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 # Caching only applies to RPC methods, so prices for open trades are still
# refreshed once every iteration. # refreshed once every iteration.
self._sell_rate_cache: TTLCache = TTLCache(maxsize=100, ttl=1800) self._exit_rate_cache: TTLCache = TTLCache(maxsize=100, ttl=1800)
self._buy_rate_cache: TTLCache = TTLCache(maxsize=100, ttl=1800) self._entry_rate_cache: TTLCache = TTLCache(maxsize=100, ttl=1800)
# Holds candles # Holds candles
self._klines: Dict[PairWithTimeframe, DataFrame] = {} self._klines: Dict[PairWithTimeframe, DataFrame] = {}
@ -1438,7 +1438,8 @@ class Exchange:
except ccxt.BaseError as e: except ccxt.BaseError as e:
raise OperationalException(e) from 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 Calculates bid/ask target
bid rate - between current ask price and last price bid rate - between current ask price and last price
@ -1450,9 +1451,10 @@ class Exchange:
:return: float: Price :return: float: Price
:raises PricingError if orderbook price could not be determined. :raises PricingError if orderbook price could not be determined.
""" """
cache_rate: TTLCache = self._buy_rate_cache if side == "buy" else self._sell_rate_cache name = side.capitalize()
[strat_name, name] = ['bid_strategy', 'Buy'] if side == "buy" else ['ask_strategy', 'Sell'] 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: if not refresh:
rate = cache_rate.get(pair) rate = cache_rate.get(pair)
# Check if cache has been invalidated # Check if cache has been invalidated
@ -1462,27 +1464,43 @@ class Exchange:
conf_strategy = self._config.get(strat_name, {}) 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_top = conf_strategy.get('order_book_top', 1)
order_book = self.fetch_l2_order_book(pair, order_book_top) order_book = self.fetch_l2_order_book(pair, order_book_top)
logger.debug('order_book %s', order_book) logger.debug('order_book %s', order_book)
# top 1 = index 0 # top 1 = index 0
try: 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: except (IndexError, KeyError) as e:
logger.warning( logger.warning(
f"{name} Price at location {order_book_top} from orderbook could not be " f"{name} Price at location {order_book_top} from orderbook could not be "
f"determined. Orderbook: {order_book}" f"determined. Orderbook: {order_book}"
) )
raise PricingError from e raise PricingError from e
price_side = {conf_strategy['price_side'].capitalize()} logger.debug(f"{name} price from orderbook {price_side_word}"
logger.debug(f"{name} price from orderbook {price_side}"
f"side - top {order_book_top} order book {side} rate {rate:.8f}") f"side - top {order_book_top} order book {side} rate {rate:.8f}")
else: 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 = self.fetch_ticker(pair)
ticker_rate = ticker[conf_strategy['price_side']] ticker_rate = ticker[price_side]
if ticker['last'] and ticker_rate: if ticker['last'] and ticker_rate:
if side == 'buy' and ticker_rate > ticker['last']: if side == 'buy' and ticker_rate > ticker['last']:
balance = conf_strategy.get('ask_last_balance', 0.0) balance = conf_strategy.get('ask_last_balance', 0.0)

View File

@ -7,7 +7,7 @@ import traceback
from datetime import datetime, time, timezone from datetime import datetime, time, timezone
from math import isclose from math import isclose
from threading import Lock 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 from schedule import Scheduler
@ -511,7 +511,8 @@ class FreqtradeBot(LoggingMixin):
return return
else: else:
logger.debug("Max adjustment entries is set to unlimited.") 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) current_profit = trade.calc_profit_ratio(current_rate)
min_stake_amount = self.exchange.get_min_pair_stake_amount(trade.pair, 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'] time_in_force = self.strategy.order_time_in_force['entry']
[side, name] = ['sell', 'Short'] if is_short else ['buy', 'Long'] [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 pos_adjust = trade is not None
enter_limit_requested, stake_amount, leverage = self.get_valid_enter_price_and_stake( 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: if not stake_amount:
return False return False
@ -745,7 +746,7 @@ class FreqtradeBot(LoggingMixin):
def get_valid_enter_price_and_stake( def get_valid_enter_price_and_stake(
self, pair: str, price: Optional[float], stake_amount: float, self, pair: str, price: Optional[float], stake_amount: float,
side: str, trade_side: str, trade_side: Literal['long', 'short'],
entry_tag: Optional[str], entry_tag: Optional[str],
trade: Optional[Trade] trade: Optional[Trade]
) -> Tuple[float, float, float]: ) -> Tuple[float, float, float]:
@ -754,7 +755,8 @@ class FreqtradeBot(LoggingMixin):
enter_limit_requested = price enter_limit_requested = price
else: else:
# Calculate price # 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, custom_entry_price = strategy_safe_wrapper(self.strategy.custom_entry_price,
default_retval=proposed_enter_rate)( default_retval=proposed_enter_rate)(
pair=pair, current_time=datetime.now(timezone.utc), 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) enter_limit_requested = self.get_valid_price(custom_entry_price, proposed_enter_rate)
if not enter_limit_requested: if not enter_limit_requested:
raise PricingError(f'Could not determine {side} price.') raise PricingError('Could not determine entry price.')
if trade is None: if trade is None:
max_leverage = self.exchange.get_max_leverage(pair, stake_amount) max_leverage = self.exchange.get_max_leverage(pair, stake_amount)
@ -824,7 +826,8 @@ class FreqtradeBot(LoggingMixin):
current_rate = trade.open_rate_requested current_rate = trade.open_rate_requested
if self.dataprovider.runmode in (RunMode.DRY_RUN, RunMode.LIVE): 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 = { msg = {
'trade_id': trade.id, 'trade_id': trade.id,
@ -853,7 +856,8 @@ class FreqtradeBot(LoggingMixin):
""" """
Sends rpc notification when a entry order cancel occurred. 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_type = RPCMessageType.SHORT_CANCEL if trade.is_short else RPCMessageType.BUY_CANCEL
msg = { msg = {
'trade_id': trade.id, 'trade_id': trade.id,
@ -935,7 +939,8 @@ class FreqtradeBot(LoggingMixin):
) )
logger.debug('checking exit') 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): if self._check_and_execute_exit(trade, exit_rate, enter, exit_, exit_tag):
return True return True
@ -1433,7 +1438,7 @@ class FreqtradeBot(LoggingMixin):
profit_trade = trade.calc_profit(rate=profit_rate) profit_trade = trade.calc_profit(rate=profit_rate)
# Use cached rates here - it was updated seconds ago. # Use cached rates here - it was updated seconds ago.
current_rate = self.exchange.get_rate( 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) profit_ratio = trade.calc_profit_ratio(profit_rate)
gain = "profit" if profit_ratio > 0 else "loss" 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_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested
profit_trade = trade.calc_profit(rate=profit_rate) 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) profit_ratio = trade.calc_profit_ratio(profit_rate)
gain = "profit" if profit_ratio > 0 else "loss" gain = "profit" if profit_ratio > 0 else "loss"

View File

@ -171,7 +171,7 @@ class RPC:
if trade.is_open: if trade.is_open:
try: try:
current_rate = self._freqtrade.exchange.get_rate( 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): except (ExchangeError, PricingError):
current_rate = NAN current_rate = NAN
else: else:
@ -231,7 +231,7 @@ class RPC:
# calculate profit and send message to user # calculate profit and send message to user
try: try:
current_rate = self._freqtrade.exchange.get_rate( 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): except (PricingError, ExchangeError):
current_rate = NAN current_rate = NAN
trade_profit = trade.calc_profit(current_rate) trade_profit = trade.calc_profit(current_rate)
@ -485,7 +485,7 @@ class RPC:
# Get current rate # Get current rate
try: try:
current_rate = self._freqtrade.exchange.get_rate( 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): except (PricingError, ExchangeError):
current_rate = NAN current_rate = NAN
profit_ratio = trade.calc_profit_ratio(rate=current_rate) profit_ratio = trade.calc_profit_ratio(rate=current_rate)
@ -705,7 +705,7 @@ class RPC:
if not fully_canceled: if not fully_canceled:
# Get current rate and execute sell # Get current rate and execute sell
current_rate = self._freqtrade.exchange.get_rate( 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) exit_check = ExitCheckTuple(exit_type=ExitType.FORCE_SELL)
order_type = ordertype or self._freqtrade.strategy.order_types.get( order_type = ordertype or self._freqtrade.strategy.order_types.get(
"forceexit", self._freqtrade.strategy.order_types["exit"]) "forceexit", self._freqtrade.strategy.order_types["exit"])