Move get_buy_rate to exchange class
This commit is contained in:
parent
4e1425023e
commit
12916243ec
@ -22,8 +22,8 @@ from pandas import DataFrame
|
|||||||
from freqtrade.constants import DEFAULT_AMOUNT_RESERVE_PERCENT, ListPairsWithTimeframes
|
from freqtrade.constants import DEFAULT_AMOUNT_RESERVE_PERCENT, ListPairsWithTimeframes
|
||||||
from freqtrade.data.converter import ohlcv_to_dataframe, trades_dict_to_list
|
from freqtrade.data.converter import ohlcv_to_dataframe, trades_dict_to_list
|
||||||
from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError,
|
from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError,
|
||||||
InvalidOrderException, OperationalException, RetryableOrderError,
|
InvalidOrderException, OperationalException, PricingError,
|
||||||
TemporaryError)
|
RetryableOrderError, TemporaryError)
|
||||||
from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, BAD_EXCHANGES,
|
from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, BAD_EXCHANGES,
|
||||||
EXCHANGE_HAS_OPTIONAL, EXCHANGE_HAS_REQUIRED, retrier,
|
EXCHANGE_HAS_OPTIONAL, EXCHANGE_HAS_REQUIRED, retrier,
|
||||||
retrier_async)
|
retrier_async)
|
||||||
@ -88,6 +88,7 @@ class Exchange:
|
|||||||
|
|
||||||
# Cache for 10 minutes ...
|
# Cache for 10 minutes ...
|
||||||
self._fetch_tickers_cache: TTLCache = TTLCache(maxsize=1, ttl=60 * 10)
|
self._fetch_tickers_cache: TTLCache = TTLCache(maxsize=1, ttl=60 * 10)
|
||||||
|
self._buy_rate_cache: TTLCache = TTLCache(maxsize=100, ttl=1800)
|
||||||
|
|
||||||
# Holds candles
|
# Holds candles
|
||||||
self._klines: Dict[Tuple[str, str], DataFrame] = {}
|
self._klines: Dict[Tuple[str, str], DataFrame] = {}
|
||||||
@ -911,6 +912,52 @@ class Exchange:
|
|||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e) from e
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
|
def get_buy_rate(self, pair: str, refresh: bool) -> float:
|
||||||
|
"""
|
||||||
|
Calculates bid target between current ask price and last price
|
||||||
|
:param pair: Pair to get rate for
|
||||||
|
:param refresh: allow cached data
|
||||||
|
:return: float: Price
|
||||||
|
:raises PricingError if orderbook price could not be determined.
|
||||||
|
"""
|
||||||
|
if not refresh:
|
||||||
|
rate = self._buy_rate_cache.get(pair)
|
||||||
|
# Check if cache has been invalidated
|
||||||
|
if rate:
|
||||||
|
logger.debug(f"Using cached buy rate for {pair}.")
|
||||||
|
return rate
|
||||||
|
|
||||||
|
bid_strategy = self._config.get('bid_strategy', {})
|
||||||
|
if 'use_order_book' in bid_strategy and bid_strategy.get('use_order_book', False):
|
||||||
|
|
||||||
|
order_book_top = bid_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_from_l2 = order_book[f"{bid_strategy['price_side']}s"][order_book_top - 1][0]
|
||||||
|
except (IndexError, KeyError) as e:
|
||||||
|
logger.warning(
|
||||||
|
"Buy Price from orderbook could not be determined."
|
||||||
|
f"Orderbook: {order_book}"
|
||||||
|
)
|
||||||
|
raise PricingError from e
|
||||||
|
logger.info(f"Buy price from orderbook {bid_strategy['price_side'].capitalize()} side "
|
||||||
|
f"- top {order_book_top} order book buy rate {rate_from_l2:.8f}")
|
||||||
|
used_rate = rate_from_l2
|
||||||
|
else:
|
||||||
|
logger.info(f"Using Last {bid_strategy['price_side'].capitalize()} / Last Price")
|
||||||
|
ticker = self.fetch_ticker(pair)
|
||||||
|
ticker_rate = ticker[bid_strategy['price_side']]
|
||||||
|
if ticker['last'] and ticker_rate > ticker['last']:
|
||||||
|
balance = bid_strategy['ask_last_balance']
|
||||||
|
ticker_rate = ticker_rate + balance * (ticker['last'] - ticker_rate)
|
||||||
|
used_rate = ticker_rate
|
||||||
|
|
||||||
|
self._buy_rate_cache[pair] = used_rate
|
||||||
|
|
||||||
|
return used_rate
|
||||||
|
|
||||||
# Fee handling
|
# Fee handling
|
||||||
|
|
||||||
@retrier
|
@retrier
|
||||||
|
@ -62,7 +62,6 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
# 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._sell_rate_cache: TTLCache = TTLCache(maxsize=100, ttl=1800)
|
||||||
self._buy_rate_cache: TTLCache = TTLCache(maxsize=100, ttl=1800)
|
|
||||||
|
|
||||||
self.strategy: IStrategy = StrategyResolver.load_strategy(self.config)
|
self.strategy: IStrategy = StrategyResolver.load_strategy(self.config)
|
||||||
|
|
||||||
@ -396,50 +395,6 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
|
|
||||||
return trades_created
|
return trades_created
|
||||||
|
|
||||||
def get_buy_rate(self, pair: str, refresh: bool) -> float:
|
|
||||||
"""
|
|
||||||
Calculates bid target between current ask price and last price
|
|
||||||
:param pair: Pair to get rate for
|
|
||||||
:param refresh: allow cached data
|
|
||||||
:return: float: Price
|
|
||||||
"""
|
|
||||||
if not refresh:
|
|
||||||
rate = self._buy_rate_cache.get(pair)
|
|
||||||
# Check if cache has been invalidated
|
|
||||||
if rate:
|
|
||||||
logger.debug(f"Using cached buy rate for {pair}.")
|
|
||||||
return rate
|
|
||||||
|
|
||||||
bid_strategy = self.config.get('bid_strategy', {})
|
|
||||||
if 'use_order_book' in bid_strategy and bid_strategy.get('use_order_book', False):
|
|
||||||
|
|
||||||
order_book_top = bid_strategy.get('order_book_top', 1)
|
|
||||||
order_book = self.exchange.fetch_l2_order_book(pair, order_book_top)
|
|
||||||
logger.debug('order_book %s', order_book)
|
|
||||||
# top 1 = index 0
|
|
||||||
try:
|
|
||||||
rate_from_l2 = order_book[f"{bid_strategy['price_side']}s"][order_book_top - 1][0]
|
|
||||||
except (IndexError, KeyError) as e:
|
|
||||||
logger.warning(
|
|
||||||
"Buy Price from orderbook could not be determined."
|
|
||||||
f"Orderbook: {order_book}"
|
|
||||||
)
|
|
||||||
raise PricingError from e
|
|
||||||
logger.info(f"Buy price from orderbook {bid_strategy['price_side'].capitalize()} side "
|
|
||||||
f"- top {order_book_top} order book buy rate {rate_from_l2:.8f}")
|
|
||||||
used_rate = rate_from_l2
|
|
||||||
else:
|
|
||||||
logger.info(f"Using Last {bid_strategy['price_side'].capitalize()} / Last Price")
|
|
||||||
ticker = self.exchange.fetch_ticker(pair)
|
|
||||||
ticker_rate = ticker[bid_strategy['price_side']]
|
|
||||||
if ticker['last'] and ticker_rate > ticker['last']:
|
|
||||||
balance = bid_strategy['ask_last_balance']
|
|
||||||
ticker_rate = ticker_rate + balance * (ticker['last'] - ticker_rate)
|
|
||||||
used_rate = ticker_rate
|
|
||||||
|
|
||||||
self._buy_rate_cache[pair] = used_rate
|
|
||||||
|
|
||||||
return used_rate
|
|
||||||
|
|
||||||
def create_trade(self, pair: str) -> bool:
|
def create_trade(self, pair: str) -> bool:
|
||||||
"""
|
"""
|
||||||
@ -532,7 +487,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
buy_limit_requested = price
|
buy_limit_requested = price
|
||||||
else:
|
else:
|
||||||
# Calculate price
|
# Calculate price
|
||||||
buy_limit_requested = self.get_buy_rate(pair, True)
|
buy_limit_requested = self.exchange.get_buy_rate(pair, True)
|
||||||
|
|
||||||
if not buy_limit_requested:
|
if not buy_limit_requested:
|
||||||
raise PricingError('Could not determine buy price.')
|
raise PricingError('Could not determine buy price.')
|
||||||
@ -657,7 +612,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
"""
|
"""
|
||||||
Sends rpc notification when a buy cancel occurred.
|
Sends rpc notification when a buy cancel occurred.
|
||||||
"""
|
"""
|
||||||
current_rate = self.get_buy_rate(trade.pair, False)
|
current_rate = self.exchange.get_buy_rate(trade.pair, False)
|
||||||
|
|
||||||
msg = {
|
msg = {
|
||||||
'trade_id': trade.id,
|
'trade_id': trade.id,
|
||||||
|
@ -1684,6 +1684,50 @@ def test_fetch_l2_order_book_exception(default_conf, mocker, exchange_name):
|
|||||||
exchange.fetch_l2_order_book(pair='ETH/BTC', limit=50)
|
exchange.fetch_l2_order_book(pair='ETH/BTC', limit=50)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("side,ask,bid,last,last_ab,expected", [
|
||||||
|
('ask', 20, 19, 10, 0.0, 20), # Full ask side
|
||||||
|
('ask', 20, 19, 10, 1.0, 10), # Full last side
|
||||||
|
('ask', 20, 19, 10, 0.5, 15), # Between ask and last
|
||||||
|
('ask', 20, 19, 10, 0.7, 13), # Between ask and last
|
||||||
|
('ask', 20, 19, 10, 0.3, 17), # Between ask and last
|
||||||
|
('ask', 5, 6, 10, 1.0, 5), # last bigger than ask
|
||||||
|
('ask', 5, 6, 10, 0.5, 5), # last bigger than ask
|
||||||
|
('ask', 10, 20, None, 0.5, 10), # last not available - uses ask
|
||||||
|
('ask', 4, 5, None, 0.5, 4), # last not available - uses ask
|
||||||
|
('ask', 4, 5, None, 1, 4), # last not available - uses ask
|
||||||
|
('ask', 4, 5, None, 0, 4), # last not available - uses ask
|
||||||
|
('bid', 21, 20, 10, 0.0, 20), # Full bid side
|
||||||
|
('bid', 21, 20, 10, 1.0, 10), # Full last side
|
||||||
|
('bid', 21, 20, 10, 0.5, 15), # Between bid and last
|
||||||
|
('bid', 21, 20, 10, 0.7, 13), # Between bid and last
|
||||||
|
('bid', 21, 20, 10, 0.3, 17), # Between bid and last
|
||||||
|
('bid', 6, 5, 10, 1.0, 5), # last bigger than bid
|
||||||
|
('bid', 6, 5, 10, 0.5, 5), # last bigger than bid
|
||||||
|
('bid', 21, 20, None, 0.5, 20), # last not available - uses bid
|
||||||
|
('bid', 6, 5, None, 0.5, 5), # last not available - uses bid
|
||||||
|
('bid', 6, 5, None, 1, 5), # last not available - uses bid
|
||||||
|
('bid', 6, 5, None, 0, 5), # last not available - uses bid
|
||||||
|
])
|
||||||
|
def test_get_buy_rate(mocker, default_conf, caplog, side, ask, bid,
|
||||||
|
last, last_ab, expected) -> None:
|
||||||
|
caplog.set_level(logging.DEBUG)
|
||||||
|
default_conf['bid_strategy']['ask_last_balance'] = last_ab
|
||||||
|
default_conf['bid_strategy']['price_side'] = side
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker',
|
||||||
|
return_value={'ask': ask, 'last': last, 'bid': bid})
|
||||||
|
|
||||||
|
assert exchange.get_buy_rate('ETH/BTC', True) == expected
|
||||||
|
assert not log_has("Using cached buy rate for ETH/BTC.", caplog)
|
||||||
|
|
||||||
|
assert exchange.get_buy_rate('ETH/BTC', False) == expected
|
||||||
|
assert log_has("Using cached buy rate for ETH/BTC.", caplog)
|
||||||
|
# Running a 2nd time with Refresh on!
|
||||||
|
caplog.clear()
|
||||||
|
assert exchange.get_buy_rate('ETH/BTC', True) == expected
|
||||||
|
assert not log_has("Using cached buy rate for ETH/BTC.", caplog)
|
||||||
|
|
||||||
|
|
||||||
def make_fetch_ohlcv_mock(data):
|
def make_fetch_ohlcv_mock(data):
|
||||||
def fetch_ohlcv_mock(pair, timeframe, since):
|
def fetch_ohlcv_mock(pair, timeframe, since):
|
||||||
if since:
|
if since:
|
||||||
|
@ -751,49 +751,6 @@ def test_process_informative_pairs_added(default_conf, ticker, mocker) -> None:
|
|||||||
assert ("ETH/BTC", default_conf["timeframe"]) in refresh_mock.call_args[0][0]
|
assert ("ETH/BTC", default_conf["timeframe"]) in refresh_mock.call_args[0][0]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("side,ask,bid,last,last_ab,expected", [
|
|
||||||
('ask', 20, 19, 10, 0.0, 20), # Full ask side
|
|
||||||
('ask', 20, 19, 10, 1.0, 10), # Full last side
|
|
||||||
('ask', 20, 19, 10, 0.5, 15), # Between ask and last
|
|
||||||
('ask', 20, 19, 10, 0.7, 13), # Between ask and last
|
|
||||||
('ask', 20, 19, 10, 0.3, 17), # Between ask and last
|
|
||||||
('ask', 5, 6, 10, 1.0, 5), # last bigger than ask
|
|
||||||
('ask', 5, 6, 10, 0.5, 5), # last bigger than ask
|
|
||||||
('ask', 10, 20, None, 0.5, 10), # last not available - uses ask
|
|
||||||
('ask', 4, 5, None, 0.5, 4), # last not available - uses ask
|
|
||||||
('ask', 4, 5, None, 1, 4), # last not available - uses ask
|
|
||||||
('ask', 4, 5, None, 0, 4), # last not available - uses ask
|
|
||||||
('bid', 21, 20, 10, 0.0, 20), # Full bid side
|
|
||||||
('bid', 21, 20, 10, 1.0, 10), # Full last side
|
|
||||||
('bid', 21, 20, 10, 0.5, 15), # Between bid and last
|
|
||||||
('bid', 21, 20, 10, 0.7, 13), # Between bid and last
|
|
||||||
('bid', 21, 20, 10, 0.3, 17), # Between bid and last
|
|
||||||
('bid', 6, 5, 10, 1.0, 5), # last bigger than bid
|
|
||||||
('bid', 6, 5, 10, 0.5, 5), # last bigger than bid
|
|
||||||
('bid', 21, 20, None, 0.5, 20), # last not available - uses bid
|
|
||||||
('bid', 6, 5, None, 0.5, 5), # last not available - uses bid
|
|
||||||
('bid', 6, 5, None, 1, 5), # last not available - uses bid
|
|
||||||
('bid', 6, 5, None, 0, 5), # last not available - uses bid
|
|
||||||
])
|
|
||||||
def test_get_buy_rate(mocker, default_conf, caplog, side, ask, bid,
|
|
||||||
last, last_ab, expected) -> None:
|
|
||||||
caplog.set_level(logging.DEBUG)
|
|
||||||
default_conf['bid_strategy']['ask_last_balance'] = last_ab
|
|
||||||
default_conf['bid_strategy']['price_side'] = side
|
|
||||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker',
|
|
||||||
return_value={'ask': ask, 'last': last, 'bid': bid})
|
|
||||||
|
|
||||||
assert freqtrade.get_buy_rate('ETH/BTC', True) == expected
|
|
||||||
assert not log_has("Using cached buy rate for ETH/BTC.", caplog)
|
|
||||||
|
|
||||||
assert freqtrade.get_buy_rate('ETH/BTC', False) == expected
|
|
||||||
assert log_has("Using cached buy rate for ETH/BTC.", caplog)
|
|
||||||
# Running a 2nd time with Refresh on!
|
|
||||||
caplog.clear()
|
|
||||||
assert freqtrade.get_buy_rate('ETH/BTC', True) == expected
|
|
||||||
assert not log_has("Using cached buy rate for ETH/BTC.", caplog)
|
|
||||||
|
|
||||||
|
|
||||||
def test_execute_buy(mocker, default_conf, fee, limit_buy_order, limit_buy_order_open) -> None:
|
def test_execute_buy(mocker, default_conf, fee, limit_buy_order, limit_buy_order_open) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
|
Loading…
Reference in New Issue
Block a user