diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 7b5c0c753..d41c78921 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -134,8 +134,8 @@ def get_balances(): return _API.get_balances() -def get_ticker(pair: str) -> dict: - return _API.get_ticker(pair) +def get_ticker(pair: str, refresh: Optional[bool] = True) -> dict: + return _API.get_ticker(pair, refresh) @cached(TTLCache(maxsize=100, ttl=30)) diff --git a/freqtrade/exchange/bittrex.py b/freqtrade/exchange/bittrex.py index 3714de070..4883db037 100644 --- a/freqtrade/exchange/bittrex.py +++ b/freqtrade/exchange/bittrex.py @@ -1,5 +1,5 @@ import logging -from typing import List, Dict +from typing import List, Dict, Optional from bittrex.bittrex import Bittrex as _Bittrex, API_V2_0, API_V1_1 from requests.exceptions import ContentDecodingError @@ -38,6 +38,7 @@ class Bittrex(Exchange): calls_per_second=1, api_version=API_V2_0, ) + self.cached_ticker = {} @staticmethod def _validate_response(response) -> None: @@ -95,26 +96,29 @@ class Bittrex(Exchange): raise OperationalException('{message}'.format(message=data['message'])) return data['result'] - def get_ticker(self, pair: str) -> dict: - data = _API.get_ticker(pair.replace('_', '-')) - if not data['success']: - Bittrex._validate_response(data) - raise OperationalException('{message} params=({pair})'.format( - message=data['message'], - pair=pair)) + def get_ticker(self, pair: str, refresh: Optional[bool] = True) -> dict: + if refresh or pair not in self.cached_ticker.keys(): + data = _API.get_ticker(pair.replace('_', '-')) + if not data['success']: + Bittrex._validate_response(data) + raise OperationalException('{message} params=({pair})'.format( + message=data['message'], + pair=pair)) - if not data.get('result') \ - or not data['result'].get('Bid') \ - or not data['result'].get('Ask') \ - or not data['result'].get('Last'): - raise ContentDecodingError('{message} params=({pair})'.format( - message='Got invalid response from bittrex', - pair=pair)) - return { - 'bid': float(data['result']['Bid']), - 'ask': float(data['result']['Ask']), - 'last': float(data['result']['Last']), - } + if not data.get('result') \ + or not data['result'].get('Bid') \ + or not data['result'].get('Ask') \ + or not data['result'].get('Last'): + raise ContentDecodingError('{message} params=({pair})'.format( + message='Got invalid response from bittrex', + pair=pair)) + # Update the pair + self.cached_ticker[pair] = { + 'bid': float(data['result']['Bid']), + 'ask': float(data['result']['Ask']), + 'last': float(data['result']['Last']), + } + return self.cached_ticker[pair] def get_ticker_history(self, pair: str, tick_interval: int) -> List[Dict]: if tick_interval == 1: diff --git a/freqtrade/exchange/interface.py b/freqtrade/exchange/interface.py index a46b3c054..1be84abe5 100644 --- a/freqtrade/exchange/interface.py +++ b/freqtrade/exchange/interface.py @@ -1,5 +1,5 @@ from abc import ABC, abstractmethod -from typing import List, Dict +from typing import List, Dict, Optional class Exchange(ABC): @@ -62,10 +62,11 @@ class Exchange(ABC): """ @abstractmethod - def get_ticker(self, pair: str) -> dict: + def get_ticker(self, pair: str, refresh: Optional[bool] = True) -> dict: """ Gets ticker for given pair. :param pair: Pair as str, format: BTC_ETC + :param refresh: Shall we query a new value or a cached value is enough :return: dict, format: { 'bid': float, 'ask': float, diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index e5e18ad5b..4d7957f59 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -140,7 +140,7 @@ def _status(bot: Bot, update: Update) -> None: if trade.open_order_id: order = exchange.get_order(trade.open_order_id) # calculate profit and send message to user - current_rate = exchange.get_ticker(trade.pair)['bid'] + current_rate = exchange.get_ticker(trade.pair, False)['bid'] current_profit = trade.calc_profit_percent(current_rate) fmt_close_profit = '{:.2f}%'.format( round(trade.close_profit * 100, 2) @@ -193,7 +193,7 @@ def _status_table(bot: Bot, update: Update) -> None: trades_list = [] for trade in trades: # calculate profit and send message to user - current_rate = exchange.get_ticker(trade.pair)['bid'] + current_rate = exchange.get_ticker(trade.pair, False)['bid'] trades_list.append([ trade.id, trade.pair, @@ -301,7 +301,7 @@ def _profit(bot: Bot, update: Update) -> None: profit_closed_percent.append(profit_percent) else: # Get current rate - current_rate = exchange.get_ticker(trade.pair)['bid'] + current_rate = exchange.get_ticker(trade.pair, False)['bid'] profit_percent = trade.calc_profit_percent(rate=current_rate) profit_all_coin.append(trade.calc_profit(rate=Decimal(trade.close_rate or current_rate))) @@ -579,7 +579,7 @@ def _exec_forcesell(trade: Trade) -> None: return # Get current rate and execute sell - current_rate = exchange.get_ticker(trade.pair)['bid'] + current_rate = exchange.get_ticker(trade.pair, False)['bid'] from freqtrade.main import execute_sell execute_sell(trade, current_rate) diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 2da657642..0a900e7c9 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -11,7 +11,8 @@ from freqtrade.exchange import init, validate_pairs, buy, sell, get_balance, get def test_init(default_conf, mocker, caplog): - mocker.patch('freqtrade.exchange.validate_pairs', side_effect=lambda s: True) + mocker.patch('freqtrade.exchange.validate_pairs', + side_effect=lambda s: True) init(config=default_conf) assert ('freqtrade.exchange', logging.INFO, @@ -25,7 +26,7 @@ def test_init_exception(default_conf, mocker): with pytest.raises( OperationalException, match='Exchange {} is not supported'.format(default_conf['exchange']['name'])): - init(config=default_conf) + init(config=default_conf) def test_validate_pairs(default_conf, mocker): @@ -49,7 +50,8 @@ def test_validate_pairs_not_available(default_conf, mocker): def test_validate_pairs_not_compatible(default_conf, mocker): api_mock = MagicMock() - api_mock.get_markets = MagicMock(return_value=['BTC_ETH', 'BTC_TKN', 'BTC_TRST', 'BTC_SWT']) + api_mock.get_markets = MagicMock( + return_value=['BTC_ETH', 'BTC_TKN', 'BTC_TRST', 'BTC_SWT']) default_conf['stake_currency'] = 'ETH' mocker.patch('freqtrade.exchange._API', api_mock) mocker.patch.dict('freqtrade.exchange._CONF', default_conf) @@ -79,7 +81,8 @@ def test_buy_dry_run(default_conf, mocker): def test_buy_prod(default_conf, mocker): api_mock = MagicMock() - api_mock.buy = MagicMock(return_value='dry_run_buy_{}'.format(randint(0, 10**6))) + api_mock.buy = MagicMock( + return_value='dry_run_buy_{}'.format(randint(0, 10**6))) mocker.patch('freqtrade.exchange._API', api_mock) default_conf['dry_run'] = False @@ -97,7 +100,8 @@ def test_sell_dry_run(default_conf, mocker): def test_sell_prod(default_conf, mocker): api_mock = MagicMock() - api_mock.sell = MagicMock(return_value='dry_run_sell_{}'.format(randint(0, 10**6))) + api_mock.sell = MagicMock( + return_value='dry_run_sell_{}'.format(randint(0, 10**6))) mocker.patch('freqtrade.exchange._API', api_mock) default_conf['dry_run'] = False @@ -141,7 +145,8 @@ def test_get_balances_prod(default_conf, mocker): } api_mock = MagicMock() - api_mock.get_balances = MagicMock(return_value=[balance_item, balance_item, balance_item]) + api_mock.get_balances = MagicMock( + return_value=[balance_item, balance_item, balance_item]) mocker.patch('freqtrade.exchange._API', api_mock) default_conf['dry_run'] = False @@ -157,13 +162,29 @@ def test_get_balances_prod(default_conf, mocker): def test_get_ticker(mocker, ticker): api_mock = MagicMock() - api_mock.get_ticker = MagicMock(return_value=ticker()) - mocker.patch('freqtrade.exchange._API', api_mock) + tick = {"success": True, 'result': {'Bid': 0.00001098, 'Ask': 0.00001099, 'Last': 0.0001}} + api_mock.get_ticker = MagicMock(return_value=tick) + mocker.patch('freqtrade.exchange.bittrex._API', api_mock) + # retrieve original ticker ticker = get_ticker(pair='BTC_ETH') assert ticker['bid'] == 0.00001098 assert ticker['ask'] == 0.00001099 + + # change the ticker + tick = {"success": True, 'result': {"Bid": 0.5, "Ask": 1, "Last": 42}} + api_mock.get_ticker = MagicMock(return_value=tick) + mocker.patch('freqtrade.exchange.bittrex._API', api_mock) + + # if not caching the result we should get the same ticker + ticker = get_ticker(pair='BTC_ETH', refresh=False) assert ticker['bid'] == 0.00001098 + assert ticker['ask'] == 0.00001099 + + # force ticker refresh + ticker = get_ticker(pair='BTC_ETH', refresh=True) + assert ticker['bid'] == 0.5 + assert ticker['ask'] == 1 def test_cancel_order_dry_run(default_conf, mocker): @@ -174,7 +195,8 @@ def test_cancel_order_dry_run(default_conf, mocker): def test_get_name(default_conf, mocker): - mocker.patch('freqtrade.exchange.validate_pairs', side_effect=lambda s: True) + mocker.patch('freqtrade.exchange.validate_pairs', + side_effect=lambda s: True) default_conf['exchange']['name'] = 'bittrex' init(default_conf) @@ -182,7 +204,8 @@ def test_get_name(default_conf, mocker): def test_get_fee(default_conf, mocker): - mocker.patch('freqtrade.exchange.validate_pairs', side_effect=lambda s: True) + mocker.patch('freqtrade.exchange.validate_pairs', + side_effect=lambda s: True) init(default_conf) assert get_fee() == 0.0025