Move get_sell_rate to exchange class
This commit is contained in:
parent
12916243ec
commit
bd1984386e
@ -88,6 +88,10 @@ class Exchange:
|
||||
|
||||
# Cache for 10 minutes ...
|
||||
self._fetch_tickers_cache: TTLCache = TTLCache(maxsize=1, ttl=60 * 10)
|
||||
# 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)
|
||||
|
||||
# Holds candles
|
||||
@ -912,6 +916,15 @@ class Exchange:
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
def _order_book_gen(self, pair: str, side: str, order_book_max: int = 1,
|
||||
order_book_min: int = 1):
|
||||
"""
|
||||
Helper generator to query orderbook in loop (used for early sell-order placing)
|
||||
"""
|
||||
order_book = self.fetch_l2_order_book(pair, order_book_max)
|
||||
for i in range(order_book_min, order_book_max + 1):
|
||||
yield order_book[side][i - 1][0]
|
||||
|
||||
def get_buy_rate(self, pair: str, refresh: bool) -> float:
|
||||
"""
|
||||
Calculates bid target between current ask price and last price
|
||||
@ -958,6 +971,46 @@ class Exchange:
|
||||
|
||||
return used_rate
|
||||
|
||||
def get_sell_rate(self, pair: str, refresh: bool) -> float:
|
||||
"""
|
||||
Get sell rate - either using ticker bid or first bid based on orderbook
|
||||
or remain static in any other case since it's not updating.
|
||||
:param pair: Pair to get rate for
|
||||
:param refresh: allow cached data
|
||||
:return: Bid rate
|
||||
:raises PricingError if price could not be determined.
|
||||
"""
|
||||
if not refresh:
|
||||
rate = self._sell_rate_cache.get(pair)
|
||||
# Check if cache has been invalidated
|
||||
if rate:
|
||||
logger.debug(f"Using cached sell rate for {pair}.")
|
||||
return rate
|
||||
|
||||
ask_strategy = self._config.get('ask_strategy', {})
|
||||
if ask_strategy.get('use_order_book', False):
|
||||
# This code is only used for notifications, selling uses the generator directly
|
||||
logger.info(
|
||||
f"Getting price from order book {ask_strategy['price_side'].capitalize()} side."
|
||||
)
|
||||
try:
|
||||
rate = next(self._order_book_gen(pair, f"{ask_strategy['price_side']}s"))
|
||||
except (IndexError, KeyError) as e:
|
||||
logger.warning("Sell Price at location from orderbook could not be determined.")
|
||||
raise PricingError from e
|
||||
else:
|
||||
ticker = self.fetch_ticker(pair)
|
||||
ticker_rate = ticker[ask_strategy['price_side']]
|
||||
if ticker['last'] and ticker_rate < ticker['last']:
|
||||
balance = ask_strategy.get('bid_last_balance', 0.0)
|
||||
ticker_rate = ticker_rate - balance * (ticker_rate - ticker['last'])
|
||||
rate = ticker_rate
|
||||
|
||||
if rate is None:
|
||||
raise PricingError(f"Sell-Rate for {pair} was empty.")
|
||||
self._sell_rate_cache[pair] = rate
|
||||
return rate
|
||||
|
||||
# Fee handling
|
||||
|
||||
@retrier
|
||||
|
@ -10,7 +10,6 @@ from threading import Lock
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import arrow
|
||||
from cachetools import TTLCache
|
||||
|
||||
from freqtrade import __version__, constants
|
||||
from freqtrade.configuration import validate_config_consistency
|
||||
@ -58,11 +57,6 @@ class FreqtradeBot(LoggingMixin):
|
||||
# Init objects
|
||||
self.config = config
|
||||
|
||||
# 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.strategy: IStrategy = StrategyResolver.load_strategy(self.config)
|
||||
|
||||
# Check config consistency here since strategies can set certain options
|
||||
@ -395,7 +389,6 @@ class FreqtradeBot(LoggingMixin):
|
||||
|
||||
return trades_created
|
||||
|
||||
|
||||
def create_trade(self, pair: str) -> bool:
|
||||
"""
|
||||
Check the implemented trading strategy for buy signals.
|
||||
@ -678,56 +671,6 @@ class FreqtradeBot(LoggingMixin):
|
||||
|
||||
return trades_closed
|
||||
|
||||
def _order_book_gen(self, pair: str, side: str, order_book_max: int = 1,
|
||||
order_book_min: int = 1):
|
||||
"""
|
||||
Helper generator to query orderbook in loop (used for early sell-order placing)
|
||||
"""
|
||||
order_book = self.exchange.fetch_l2_order_book(pair, order_book_max)
|
||||
for i in range(order_book_min, order_book_max + 1):
|
||||
yield order_book[side][i - 1][0]
|
||||
|
||||
def get_sell_rate(self, pair: str, refresh: bool) -> float:
|
||||
"""
|
||||
Get sell rate - either using ticker bid or first bid based on orderbook
|
||||
The orderbook portion is only used for rpc messaging, which would otherwise fail
|
||||
for BitMex (has no bid/ask in fetch_ticker)
|
||||
or remain static in any other case since it's not updating.
|
||||
:param pair: Pair to get rate for
|
||||
:param refresh: allow cached data
|
||||
:return: Bid rate
|
||||
"""
|
||||
if not refresh:
|
||||
rate = self._sell_rate_cache.get(pair)
|
||||
# Check if cache has been invalidated
|
||||
if rate:
|
||||
logger.debug(f"Using cached sell rate for {pair}.")
|
||||
return rate
|
||||
|
||||
ask_strategy = self.config.get('ask_strategy', {})
|
||||
if ask_strategy.get('use_order_book', False):
|
||||
# This code is only used for notifications, selling uses the generator directly
|
||||
logger.info(
|
||||
f"Getting price from order book {ask_strategy['price_side'].capitalize()} side."
|
||||
)
|
||||
try:
|
||||
rate = next(self._order_book_gen(pair, f"{ask_strategy['price_side']}s"))
|
||||
except (IndexError, KeyError) as e:
|
||||
logger.warning("Sell Price at location from orderbook could not be determined.")
|
||||
raise PricingError from e
|
||||
else:
|
||||
ticker = self.exchange.fetch_ticker(pair)
|
||||
ticker_rate = ticker[ask_strategy['price_side']]
|
||||
if ticker['last'] and ticker_rate < ticker['last']:
|
||||
balance = ask_strategy.get('bid_last_balance', 0.0)
|
||||
ticker_rate = ticker_rate - balance * (ticker_rate - ticker['last'])
|
||||
rate = ticker_rate
|
||||
|
||||
if rate is None:
|
||||
raise PricingError(f"Sell-Rate for {pair} was empty.")
|
||||
self._sell_rate_cache[pair] = rate
|
||||
return rate
|
||||
|
||||
def handle_trade(self, trade: Trade) -> bool:
|
||||
"""
|
||||
Sells the current pair if the threshold is reached and updates the trade record.
|
||||
@ -755,9 +698,9 @@ class FreqtradeBot(LoggingMixin):
|
||||
logger.debug(f'Using order book between {order_book_min} and {order_book_max} '
|
||||
f'for selling {trade.pair}...')
|
||||
|
||||
order_book = self._order_book_gen(trade.pair, f"{config_ask_strategy['price_side']}s",
|
||||
order_book_min=order_book_min,
|
||||
order_book_max=order_book_max)
|
||||
order_book = self.exchange._order_book_gen(
|
||||
trade.pair, f"{config_ask_strategy['price_side']}s",
|
||||
order_book_min=order_book_min, order_book_max=order_book_max)
|
||||
for i in range(order_book_min, order_book_max + 1):
|
||||
try:
|
||||
sell_rate = next(order_book)
|
||||
@ -770,14 +713,14 @@ class FreqtradeBot(LoggingMixin):
|
||||
f"{sell_rate:0.8f}")
|
||||
# Assign sell-rate to cache - otherwise sell-rate is never updated in the cache,
|
||||
# resulting in outdated RPC messages
|
||||
self._sell_rate_cache[trade.pair] = sell_rate
|
||||
self.exchange._sell_rate_cache[trade.pair] = sell_rate
|
||||
|
||||
if self._check_and_execute_sell(trade, sell_rate, buy, sell):
|
||||
return True
|
||||
|
||||
else:
|
||||
logger.debug('checking sell')
|
||||
sell_rate = self.get_sell_rate(trade.pair, True)
|
||||
sell_rate = self.exchange.get_sell_rate(trade.pair, True)
|
||||
if self._check_and_execute_sell(trade, sell_rate, buy, sell):
|
||||
return True
|
||||
|
||||
@ -1209,7 +1152,7 @@ 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)
|
||||
# Use cached rates here - it was updated seconds ago.
|
||||
current_rate = self.get_sell_rate(trade.pair, False) if not fill else None
|
||||
current_rate = self.exchange.get_sell_rate(trade.pair, False) if not fill else None
|
||||
profit_ratio = trade.calc_profit_ratio(profit_rate)
|
||||
gain = "profit" if profit_ratio > 0 else "loss"
|
||||
|
||||
@ -1254,7 +1197,7 @@ 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.get_sell_rate(trade.pair, False)
|
||||
current_rate = self.exchange.get_sell_rate(trade.pair, False)
|
||||
profit_ratio = trade.calc_profit_ratio(profit_rate)
|
||||
gain = "profit" if profit_ratio > 0 else "loss"
|
||||
|
||||
|
@ -171,7 +171,7 @@ class RPC:
|
||||
# calculate profit and send message to user
|
||||
if trade.is_open:
|
||||
try:
|
||||
current_rate = self._freqtrade.get_sell_rate(trade.pair, False)
|
||||
current_rate = self._freqtrade.exchange.get_sell_rate(trade.pair, False)
|
||||
except (ExchangeError, PricingError):
|
||||
current_rate = NAN
|
||||
else:
|
||||
@ -230,7 +230,7 @@ class RPC:
|
||||
for trade in trades:
|
||||
# calculate profit and send message to user
|
||||
try:
|
||||
current_rate = self._freqtrade.get_sell_rate(trade.pair, False)
|
||||
current_rate = self._freqtrade.exchange.get_sell_rate(trade.pair, False)
|
||||
except (PricingError, ExchangeError):
|
||||
current_rate = NAN
|
||||
trade_percent = (100 * trade.calc_profit_ratio(current_rate))
|
||||
@ -386,7 +386,7 @@ class RPC:
|
||||
else:
|
||||
# Get current rate
|
||||
try:
|
||||
current_rate = self._freqtrade.get_sell_rate(trade.pair, False)
|
||||
current_rate = self._freqtrade.exchange.get_sell_rate(trade.pair, False)
|
||||
except (PricingError, ExchangeError):
|
||||
current_rate = NAN
|
||||
profit_ratio = trade.calc_profit_ratio(rate=current_rate)
|
||||
@ -556,7 +556,7 @@ class RPC:
|
||||
|
||||
if not fully_canceled:
|
||||
# Get current rate and execute sell
|
||||
current_rate = self._freqtrade.get_sell_rate(trade.pair, False)
|
||||
current_rate = self._freqtrade.exchange.get_sell_rate(trade.pair, False)
|
||||
sell_reason = SellCheckTuple(sell_type=SellType.FORCE_SELL)
|
||||
self._freqtrade.execute_sell(trade, current_rate, sell_reason)
|
||||
# ---- EOF def _exec_forcesell ----
|
||||
|
@ -11,7 +11,7 @@ import pytest
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.exceptions import (DDosProtection, DependencyException, InvalidOrderException,
|
||||
OperationalException, TemporaryError)
|
||||
OperationalException, PricingError, TemporaryError)
|
||||
from freqtrade.exchange import Binance, Bittrex, Exchange, Kraken
|
||||
from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, API_RETRY_COUNT,
|
||||
calculate_backoff)
|
||||
@ -1728,6 +1728,108 @@ def test_get_buy_rate(mocker, default_conf, caplog, side, ask, bid,
|
||||
assert not log_has("Using cached buy rate for ETH/BTC.", caplog)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('side,ask,bid,last,last_ab,expected', [
|
||||
('bid', 12.0, 11.0, 11.5, 0.0, 11.0), # full bid side
|
||||
('bid', 12.0, 11.0, 11.5, 1.0, 11.5), # full last side
|
||||
('bid', 12.0, 11.0, 11.5, 0.5, 11.25), # between bid and lat
|
||||
('bid', 12.0, 11.2, 10.5, 0.0, 11.2), # Last smaller than bid
|
||||
('bid', 12.0, 11.2, 10.5, 1.0, 11.2), # Last smaller than bid - uses bid
|
||||
('bid', 12.0, 11.2, 10.5, 0.5, 11.2), # Last smaller than bid - uses bid
|
||||
('bid', 0.003, 0.002, 0.005, 0.0, 0.002),
|
||||
('ask', 12.0, 11.0, 12.5, 0.0, 12.0), # full ask side
|
||||
('ask', 12.0, 11.0, 12.5, 1.0, 12.5), # full last side
|
||||
('ask', 12.0, 11.0, 12.5, 0.5, 12.25), # between bid and lat
|
||||
('ask', 12.2, 11.2, 10.5, 0.0, 12.2), # Last smaller than ask
|
||||
('ask', 12.0, 11.0, 10.5, 1.0, 12.0), # Last smaller than ask - uses ask
|
||||
('ask', 12.0, 11.2, 10.5, 0.5, 12.0), # Last smaller than ask - uses ask
|
||||
('ask', 10.0, 11.0, 11.0, 0.0, 10.0),
|
||||
('ask', 10.11, 11.2, 11.0, 0.0, 10.11),
|
||||
('ask', 0.001, 0.002, 11.0, 0.0, 0.001),
|
||||
('ask', 0.006, 1.0, 11.0, 0.0, 0.006),
|
||||
])
|
||||
def test_get_sell_rate(default_conf, mocker, caplog, side, bid, ask,
|
||||
last, last_ab, expected) -> None:
|
||||
caplog.set_level(logging.DEBUG)
|
||||
|
||||
default_conf['ask_strategy']['price_side'] = side
|
||||
default_conf['ask_strategy']['bid_last_balance'] = last_ab
|
||||
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker',
|
||||
return_value={'ask': ask, 'bid': bid, 'last': last})
|
||||
pair = "ETH/BTC"
|
||||
|
||||
# Test regular mode
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
rate = exchange.get_sell_rate(pair, True)
|
||||
assert not log_has("Using cached sell rate for ETH/BTC.", caplog)
|
||||
assert isinstance(rate, float)
|
||||
assert rate == expected
|
||||
# Use caching
|
||||
rate = exchange.get_sell_rate(pair, False)
|
||||
assert rate == expected
|
||||
assert log_has("Using cached sell rate for ETH/BTC.", caplog)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('side,expected', [
|
||||
('bid', 0.043936), # Value from order_book_l2 fiture - bids side
|
||||
('ask', 0.043949), # Value from order_book_l2 fiture - asks side
|
||||
])
|
||||
def test_get_sell_rate_orderbook(default_conf, mocker, caplog, side, expected, order_book_l2):
|
||||
caplog.set_level(logging.DEBUG)
|
||||
# Test orderbook mode
|
||||
default_conf['ask_strategy']['price_side'] = side
|
||||
default_conf['ask_strategy']['use_order_book'] = True
|
||||
default_conf['ask_strategy']['order_book_min'] = 1
|
||||
default_conf['ask_strategy']['order_book_max'] = 2
|
||||
pair = "ETH/BTC"
|
||||
mocker.patch('freqtrade.exchange.Exchange.fetch_l2_order_book', order_book_l2)
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
rate = exchange.get_sell_rate(pair, True)
|
||||
assert not log_has("Using cached sell rate for ETH/BTC.", caplog)
|
||||
assert isinstance(rate, float)
|
||||
assert rate == expected
|
||||
rate = exchange.get_sell_rate(pair, False)
|
||||
assert rate == expected
|
||||
assert log_has("Using cached sell rate for ETH/BTC.", caplog)
|
||||
|
||||
|
||||
def test_get_sell_rate_orderbook_exception(default_conf, mocker, caplog):
|
||||
# Test orderbook mode
|
||||
default_conf['ask_strategy']['price_side'] = 'ask'
|
||||
default_conf['ask_strategy']['use_order_book'] = True
|
||||
default_conf['ask_strategy']['order_book_min'] = 1
|
||||
default_conf['ask_strategy']['order_book_max'] = 2
|
||||
pair = "ETH/BTC"
|
||||
# Test What happens if the exchange returns an empty orderbook.
|
||||
mocker.patch('freqtrade.exchange.Exchange.fetch_l2_order_book',
|
||||
return_value={'bids': [[]], 'asks': [[]]})
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
with pytest.raises(PricingError):
|
||||
exchange.get_sell_rate(pair, True)
|
||||
assert log_has("Sell Price at location from orderbook could not be determined.", caplog)
|
||||
|
||||
|
||||
def test_get_sell_rate_exception(default_conf, mocker, caplog):
|
||||
# Ticker on one side can be empty in certain circumstances.
|
||||
default_conf['ask_strategy']['price_side'] = 'ask'
|
||||
pair = "ETH/BTC"
|
||||
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker',
|
||||
return_value={'ask': None, 'bid': 0.12, 'last': None})
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
with pytest.raises(PricingError, match=r"Sell-Rate for ETH/BTC was empty."):
|
||||
exchange.get_sell_rate(pair, True)
|
||||
|
||||
exchange._config['ask_strategy']['price_side'] = 'bid'
|
||||
assert exchange.get_sell_rate(pair, True) == 0.12
|
||||
# Reverse sides
|
||||
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker',
|
||||
return_value={'ask': 0.13, 'bid': None, 'last': None})
|
||||
with pytest.raises(PricingError, match=r"Sell-Rate for ETH/BTC was empty."):
|
||||
exchange.get_sell_rate(pair, True)
|
||||
|
||||
exchange._config['ask_strategy']['price_side'] = 'ask'
|
||||
assert exchange.get_sell_rate(pair, True) == 0.13
|
||||
|
||||
|
||||
def make_fetch_ohlcv_mock(data):
|
||||
def fetch_ohlcv_mock(pair, timeframe, since):
|
||||
if since:
|
||||
|
@ -109,7 +109,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
||||
'exchange': 'binance',
|
||||
}
|
||||
|
||||
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate',
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_sell_rate',
|
||||
MagicMock(side_effect=ExchangeError("Pair 'ETH/BTC' not available")))
|
||||
results = rpc._rpc_trade_status()
|
||||
assert isnan(results[0]['current_profit'])
|
||||
@ -217,7 +217,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None:
|
||||
assert '-0.41% (-0.06)' == result[0][3]
|
||||
assert '-0.06' == f'{fiat_profit_sum:.2f}'
|
||||
|
||||
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate',
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_sell_rate',
|
||||
MagicMock(side_effect=ExchangeError("Pair 'ETH/BTC' not available")))
|
||||
result, headers, fiat_profit_sum = rpc._rpc_status_table(default_conf['stake_currency'], 'USD')
|
||||
assert 'instantly' == result[0][2]
|
||||
@ -427,7 +427,7 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
|
||||
assert prec_satoshi(stats['best_rate'], 6.2)
|
||||
|
||||
# Test non-available pair
|
||||
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate',
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_sell_rate',
|
||||
MagicMock(side_effect=ExchangeError("Pair 'ETH/BTC' not available")))
|
||||
stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
|
||||
assert stats['trade_count'] == 2
|
||||
|
@ -834,7 +834,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets):
|
||||
'exchange': 'binance',
|
||||
}
|
||||
|
||||
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate',
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_sell_rate',
|
||||
MagicMock(side_effect=ExchangeError("Pair 'ETH/BTC' not available")))
|
||||
|
||||
rc = client_get(client, f"{BASE_URI}/status")
|
||||
|
@ -751,7 +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]
|
||||
|
||||
|
||||
|
||||
def test_execute_buy(mocker, default_conf, fee, limit_buy_order, limit_buy_order_open) -> None:
|
||||
patch_RPCManager(mocker)
|
||||
patch_exchange(mocker)
|
||||
@ -2480,7 +2479,7 @@ def test_handle_cancel_sell_limit(mocker, default_conf, fee) -> None:
|
||||
'freqtrade.exchange.Exchange',
|
||||
cancel_order=cancel_order_mock,
|
||||
)
|
||||
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate', return_value=0.245441)
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_sell_rate', return_value=0.245441)
|
||||
|
||||
freqtrade = FreqtradeBot(default_conf)
|
||||
|
||||
@ -4029,108 +4028,6 @@ def test_order_book_ask_strategy(default_conf, limit_buy_order_open, limit_buy_o
|
||||
assert log_has('Sell Price at location 1 from orderbook could not be determined.', caplog)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('side,ask,bid,last,last_ab,expected', [
|
||||
('bid', 12.0, 11.0, 11.5, 0.0, 11.0), # full bid side
|
||||
('bid', 12.0, 11.0, 11.5, 1.0, 11.5), # full last side
|
||||
('bid', 12.0, 11.0, 11.5, 0.5, 11.25), # between bid and lat
|
||||
('bid', 12.0, 11.2, 10.5, 0.0, 11.2), # Last smaller than bid
|
||||
('bid', 12.0, 11.2, 10.5, 1.0, 11.2), # Last smaller than bid - uses bid
|
||||
('bid', 12.0, 11.2, 10.5, 0.5, 11.2), # Last smaller than bid - uses bid
|
||||
('bid', 0.003, 0.002, 0.005, 0.0, 0.002),
|
||||
('ask', 12.0, 11.0, 12.5, 0.0, 12.0), # full ask side
|
||||
('ask', 12.0, 11.0, 12.5, 1.0, 12.5), # full last side
|
||||
('ask', 12.0, 11.0, 12.5, 0.5, 12.25), # between bid and lat
|
||||
('ask', 12.2, 11.2, 10.5, 0.0, 12.2), # Last smaller than ask
|
||||
('ask', 12.0, 11.0, 10.5, 1.0, 12.0), # Last smaller than ask - uses ask
|
||||
('ask', 12.0, 11.2, 10.5, 0.5, 12.0), # Last smaller than ask - uses ask
|
||||
('ask', 10.0, 11.0, 11.0, 0.0, 10.0),
|
||||
('ask', 10.11, 11.2, 11.0, 0.0, 10.11),
|
||||
('ask', 0.001, 0.002, 11.0, 0.0, 0.001),
|
||||
('ask', 0.006, 1.0, 11.0, 0.0, 0.006),
|
||||
])
|
||||
def test_get_sell_rate(default_conf, mocker, caplog, side, bid, ask,
|
||||
last, last_ab, expected) -> None:
|
||||
caplog.set_level(logging.DEBUG)
|
||||
|
||||
default_conf['ask_strategy']['price_side'] = side
|
||||
default_conf['ask_strategy']['bid_last_balance'] = last_ab
|
||||
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker',
|
||||
return_value={'ask': ask, 'bid': bid, 'last': last})
|
||||
pair = "ETH/BTC"
|
||||
|
||||
# Test regular mode
|
||||
ft = get_patched_freqtradebot(mocker, default_conf)
|
||||
rate = ft.get_sell_rate(pair, True)
|
||||
assert not log_has("Using cached sell rate for ETH/BTC.", caplog)
|
||||
assert isinstance(rate, float)
|
||||
assert rate == expected
|
||||
# Use caching
|
||||
rate = ft.get_sell_rate(pair, False)
|
||||
assert rate == expected
|
||||
assert log_has("Using cached sell rate for ETH/BTC.", caplog)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('side,expected', [
|
||||
('bid', 0.043936), # Value from order_book_l2 fiture - bids side
|
||||
('ask', 0.043949), # Value from order_book_l2 fiture - asks side
|
||||
])
|
||||
def test_get_sell_rate_orderbook(default_conf, mocker, caplog, side, expected, order_book_l2):
|
||||
caplog.set_level(logging.DEBUG)
|
||||
# Test orderbook mode
|
||||
default_conf['ask_strategy']['price_side'] = side
|
||||
default_conf['ask_strategy']['use_order_book'] = True
|
||||
default_conf['ask_strategy']['order_book_min'] = 1
|
||||
default_conf['ask_strategy']['order_book_max'] = 2
|
||||
pair = "ETH/BTC"
|
||||
mocker.patch('freqtrade.exchange.Exchange.fetch_l2_order_book', order_book_l2)
|
||||
ft = get_patched_freqtradebot(mocker, default_conf)
|
||||
rate = ft.get_sell_rate(pair, True)
|
||||
assert not log_has("Using cached sell rate for ETH/BTC.", caplog)
|
||||
assert isinstance(rate, float)
|
||||
assert rate == expected
|
||||
rate = ft.get_sell_rate(pair, False)
|
||||
assert rate == expected
|
||||
assert log_has("Using cached sell rate for ETH/BTC.", caplog)
|
||||
|
||||
|
||||
def test_get_sell_rate_orderbook_exception(default_conf, mocker, caplog):
|
||||
# Test orderbook mode
|
||||
default_conf['ask_strategy']['price_side'] = 'ask'
|
||||
default_conf['ask_strategy']['use_order_book'] = True
|
||||
default_conf['ask_strategy']['order_book_min'] = 1
|
||||
default_conf['ask_strategy']['order_book_max'] = 2
|
||||
pair = "ETH/BTC"
|
||||
# Test What happens if the exchange returns an empty orderbook.
|
||||
mocker.patch('freqtrade.exchange.Exchange.fetch_l2_order_book',
|
||||
return_value={'bids': [[]], 'asks': [[]]})
|
||||
ft = get_patched_freqtradebot(mocker, default_conf)
|
||||
with pytest.raises(PricingError):
|
||||
ft.get_sell_rate(pair, True)
|
||||
assert log_has("Sell Price at location from orderbook could not be determined.", caplog)
|
||||
|
||||
|
||||
def test_get_sell_rate_exception(default_conf, mocker, caplog):
|
||||
# Ticker on one side can be empty in certain circumstances.
|
||||
default_conf['ask_strategy']['price_side'] = 'ask'
|
||||
pair = "ETH/BTC"
|
||||
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker',
|
||||
return_value={'ask': None, 'bid': 0.12, 'last': None})
|
||||
ft = get_patched_freqtradebot(mocker, default_conf)
|
||||
with pytest.raises(PricingError, match=r"Sell-Rate for ETH/BTC was empty."):
|
||||
ft.get_sell_rate(pair, True)
|
||||
|
||||
ft.config['ask_strategy']['price_side'] = 'bid'
|
||||
assert ft.get_sell_rate(pair, True) == 0.12
|
||||
# Reverse sides
|
||||
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker',
|
||||
return_value={'ask': 0.13, 'bid': None, 'last': None})
|
||||
with pytest.raises(PricingError, match=r"Sell-Rate for ETH/BTC was empty."):
|
||||
ft.get_sell_rate(pair, True)
|
||||
|
||||
ft.config['ask_strategy']['price_side'] = 'ask'
|
||||
assert ft.get_sell_rate(pair, True) == 0.13
|
||||
|
||||
|
||||
def test_startup_state(default_conf, mocker):
|
||||
default_conf['pairlist'] = {'method': 'VolumePairList',
|
||||
'config': {'number_assets': 20}
|
||||
|
Loading…
Reference in New Issue
Block a user