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 ...
|
# 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)
|
||||||
|
# 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._buy_rate_cache: TTLCache = TTLCache(maxsize=100, ttl=1800)
|
||||||
|
|
||||||
# Holds candles
|
# Holds candles
|
||||||
@ -912,6 +916,15 @@ class Exchange:
|
|||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e) from 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:
|
def get_buy_rate(self, pair: str, refresh: bool) -> float:
|
||||||
"""
|
"""
|
||||||
Calculates bid target between current ask price and last price
|
Calculates bid target between current ask price and last price
|
||||||
@ -958,6 +971,46 @@ class Exchange:
|
|||||||
|
|
||||||
return used_rate
|
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
|
# Fee handling
|
||||||
|
|
||||||
@retrier
|
@retrier
|
||||||
|
@ -10,7 +10,6 @@ from threading import Lock
|
|||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
from cachetools import TTLCache
|
|
||||||
|
|
||||||
from freqtrade import __version__, constants
|
from freqtrade import __version__, constants
|
||||||
from freqtrade.configuration import validate_config_consistency
|
from freqtrade.configuration import validate_config_consistency
|
||||||
@ -58,11 +57,6 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
# Init objects
|
# Init objects
|
||||||
self.config = config
|
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)
|
self.strategy: IStrategy = StrategyResolver.load_strategy(self.config)
|
||||||
|
|
||||||
# Check config consistency here since strategies can set certain options
|
# Check config consistency here since strategies can set certain options
|
||||||
@ -395,7 +389,6 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
|
|
||||||
return trades_created
|
return trades_created
|
||||||
|
|
||||||
|
|
||||||
def create_trade(self, pair: str) -> bool:
|
def create_trade(self, pair: str) -> bool:
|
||||||
"""
|
"""
|
||||||
Check the implemented trading strategy for buy signals.
|
Check the implemented trading strategy for buy signals.
|
||||||
@ -678,56 +671,6 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
|
|
||||||
return trades_closed
|
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:
|
def handle_trade(self, trade: Trade) -> bool:
|
||||||
"""
|
"""
|
||||||
Sells the current pair if the threshold is reached and updates the trade record.
|
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} '
|
logger.debug(f'Using order book between {order_book_min} and {order_book_max} '
|
||||||
f'for selling {trade.pair}...')
|
f'for selling {trade.pair}...')
|
||||||
|
|
||||||
order_book = self._order_book_gen(trade.pair, f"{config_ask_strategy['price_side']}s",
|
order_book = self.exchange._order_book_gen(
|
||||||
order_book_min=order_book_min,
|
trade.pair, f"{config_ask_strategy['price_side']}s",
|
||||||
order_book_max=order_book_max)
|
order_book_min=order_book_min, order_book_max=order_book_max)
|
||||||
for i in range(order_book_min, order_book_max + 1):
|
for i in range(order_book_min, order_book_max + 1):
|
||||||
try:
|
try:
|
||||||
sell_rate = next(order_book)
|
sell_rate = next(order_book)
|
||||||
@ -770,14 +713,14 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
f"{sell_rate:0.8f}")
|
f"{sell_rate:0.8f}")
|
||||||
# Assign sell-rate to cache - otherwise sell-rate is never updated in the cache,
|
# Assign sell-rate to cache - otherwise sell-rate is never updated in the cache,
|
||||||
# resulting in outdated RPC messages
|
# 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):
|
if self._check_and_execute_sell(trade, sell_rate, buy, sell):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
else:
|
else:
|
||||||
logger.debug('checking sell')
|
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):
|
if self._check_and_execute_sell(trade, sell_rate, buy, sell):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -1209,7 +1152,7 @@ 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)
|
||||||
# Use cached rates here - it was updated seconds ago.
|
# 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)
|
profit_ratio = trade.calc_profit_ratio(profit_rate)
|
||||||
gain = "profit" if profit_ratio > 0 else "loss"
|
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_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.get_sell_rate(trade.pair, False)
|
current_rate = self.exchange.get_sell_rate(trade.pair, 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"
|
||||||
|
|
||||||
|
@ -171,7 +171,7 @@ class RPC:
|
|||||||
# calculate profit and send message to user
|
# calculate profit and send message to user
|
||||||
if trade.is_open:
|
if trade.is_open:
|
||||||
try:
|
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):
|
except (ExchangeError, PricingError):
|
||||||
current_rate = NAN
|
current_rate = NAN
|
||||||
else:
|
else:
|
||||||
@ -230,7 +230,7 @@ class RPC:
|
|||||||
for trade in trades:
|
for trade in trades:
|
||||||
# calculate profit and send message to user
|
# calculate profit and send message to user
|
||||||
try:
|
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):
|
except (PricingError, ExchangeError):
|
||||||
current_rate = NAN
|
current_rate = NAN
|
||||||
trade_percent = (100 * trade.calc_profit_ratio(current_rate))
|
trade_percent = (100 * trade.calc_profit_ratio(current_rate))
|
||||||
@ -386,7 +386,7 @@ class RPC:
|
|||||||
else:
|
else:
|
||||||
# Get current rate
|
# Get current rate
|
||||||
try:
|
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):
|
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)
|
||||||
@ -556,7 +556,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.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)
|
sell_reason = SellCheckTuple(sell_type=SellType.FORCE_SELL)
|
||||||
self._freqtrade.execute_sell(trade, current_rate, sell_reason)
|
self._freqtrade.execute_sell(trade, current_rate, sell_reason)
|
||||||
# ---- EOF def _exec_forcesell ----
|
# ---- EOF def _exec_forcesell ----
|
||||||
|
@ -11,7 +11,7 @@ import pytest
|
|||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
from freqtrade.exceptions import (DDosProtection, DependencyException, InvalidOrderException,
|
from freqtrade.exceptions import (DDosProtection, DependencyException, InvalidOrderException,
|
||||||
OperationalException, TemporaryError)
|
OperationalException, PricingError, TemporaryError)
|
||||||
from freqtrade.exchange import Binance, Bittrex, Exchange, Kraken
|
from freqtrade.exchange import Binance, Bittrex, Exchange, Kraken
|
||||||
from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, API_RETRY_COUNT,
|
from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, API_RETRY_COUNT,
|
||||||
calculate_backoff)
|
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)
|
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 make_fetch_ohlcv_mock(data):
|
||||||
def fetch_ohlcv_mock(pair, timeframe, since):
|
def fetch_ohlcv_mock(pair, timeframe, since):
|
||||||
if since:
|
if since:
|
||||||
|
@ -109,7 +109,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
|||||||
'exchange': 'binance',
|
'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")))
|
MagicMock(side_effect=ExchangeError("Pair 'ETH/BTC' not available")))
|
||||||
results = rpc._rpc_trade_status()
|
results = rpc._rpc_trade_status()
|
||||||
assert isnan(results[0]['current_profit'])
|
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.41% (-0.06)' == result[0][3]
|
||||||
assert '-0.06' == f'{fiat_profit_sum:.2f}'
|
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")))
|
MagicMock(side_effect=ExchangeError("Pair 'ETH/BTC' not available")))
|
||||||
result, headers, fiat_profit_sum = rpc._rpc_status_table(default_conf['stake_currency'], 'USD')
|
result, headers, fiat_profit_sum = rpc._rpc_status_table(default_conf['stake_currency'], 'USD')
|
||||||
assert 'instantly' == result[0][2]
|
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)
|
assert prec_satoshi(stats['best_rate'], 6.2)
|
||||||
|
|
||||||
# Test non-available pair
|
# 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")))
|
MagicMock(side_effect=ExchangeError("Pair 'ETH/BTC' not available")))
|
||||||
stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
|
stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
|
||||||
assert stats['trade_count'] == 2
|
assert stats['trade_count'] == 2
|
||||||
|
@ -834,7 +834,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets):
|
|||||||
'exchange': 'binance',
|
'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")))
|
MagicMock(side_effect=ExchangeError("Pair 'ETH/BTC' not available")))
|
||||||
|
|
||||||
rc = client_get(client, f"{BASE_URI}/status")
|
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]
|
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:
|
def test_execute_buy(mocker, default_conf, fee, limit_buy_order, limit_buy_order_open) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
@ -2480,7 +2479,7 @@ def test_handle_cancel_sell_limit(mocker, default_conf, fee) -> None:
|
|||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
cancel_order=cancel_order_mock,
|
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)
|
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)
|
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):
|
def test_startup_state(default_conf, mocker):
|
||||||
default_conf['pairlist'] = {'method': 'VolumePairList',
|
default_conf['pairlist'] = {'method': 'VolumePairList',
|
||||||
'config': {'number_assets': 20}
|
'config': {'number_assets': 20}
|
||||||
|
Loading…
Reference in New Issue
Block a user