From dd9cb008fbc0a437576811f8d5e5021786f97bcc Mon Sep 17 00:00:00 2001 From: gcarq Date: Mon, 13 Nov 2017 21:34:47 +0100 Subject: [PATCH] refresh whitelist based on wallet health (fixes #60) Refreshs the whitelist in each iteration based on the wallet health, disabled wallets will be removed from the whitelist automatically. --- freqtrade/exchange/__init__.py | 4 +++ freqtrade/exchange/bittrex.py | 17 +++++++++++- freqtrade/exchange/interface.py | 14 ++++++++++ freqtrade/main.py | 45 +++++++++++++++++++++++++++----- freqtrade/tests/conftest.py | 33 ++++++++++++++++++++++- freqtrade/tests/test_exchange.py | 4 ++- freqtrade/tests/test_main.py | 12 ++++++--- 7 files changed, 115 insertions(+), 14 deletions(-) diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 10de18b14..a6e986f7b 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -169,3 +169,7 @@ def get_name() -> str: def get_fee() -> float: return _API.fee + + +def get_wallet_health() -> List[Dict]: + return _API.get_wallet_health() diff --git a/freqtrade/exchange/bittrex.py b/freqtrade/exchange/bittrex.py index b1d8e68fb..6f965a9b4 100644 --- a/freqtrade/exchange/bittrex.py +++ b/freqtrade/exchange/bittrex.py @@ -82,6 +82,10 @@ class Bittrex(Exchange): raise RuntimeError('{message} params=({pair})'.format( message=data['message'], pair=pair)) + if not data['result']['Bid'] or not data['result']['Ask'] or not data['result']['Last']: + raise RuntimeError('{message} params=({pair})'.format( + message=data['message'], + pair=pair)) return { 'bid': float(data['result']['Bid']), 'ask': float(data['result']['Ask']), @@ -101,7 +105,7 @@ class Bittrex(Exchange): for prop in ['C', 'V', 'O', 'H', 'L', 'T']: for tick in data['result']: if prop not in tick.keys(): - logger.warning('Required property {} not present in response'.format(prop)) + logger.warning('Required property %s not present in response', prop) return [] if not data['success']: @@ -150,3 +154,14 @@ class Bittrex(Exchange): if not data['success']: raise RuntimeError('{message}'.format(message=data['message'])) return data['result'] + + def get_wallet_health(self) -> List[Dict]: + data = _API_V2.get_wallet_health() + if not data['success']: + raise RuntimeError('{message}'.format(message=data['message'])) + return [{ + 'Currency': entry['Health']['Currency'], + 'IsActive': entry['Health']['IsActive'], + 'LastChecked': entry['Health']['LastChecked'], + 'Notice': entry['Currency'].get('Notice'), + } for entry in data['result']] diff --git a/freqtrade/exchange/interface.py b/freqtrade/exchange/interface.py index 60e6b03cd..a46b3c054 100644 --- a/freqtrade/exchange/interface.py +++ b/freqtrade/exchange/interface.py @@ -155,3 +155,17 @@ class Exchange(ABC): ... ] """ + + @abstractmethod + def get_wallet_health(self) -> List[Dict]: + """ + Returns a list of all wallet health information + :return: list, format: [ + { + 'Currency': str, + 'IsActive': bool, + 'LastChecked': str, + 'Notice': str + }, + ... + """ diff --git a/freqtrade/main.py b/freqtrade/main.py index 932b1532e..cee797be6 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -23,14 +23,45 @@ logger = logging.getLogger('freqtrade') _CONF = {} -def _process() -> bool: +def refresh_whitelist(whitelist: Optional[List[str]] = None) -> None: + """ + Check wallet health and remove pair from whitelist if necessary + :param whitelist: a new whitelist (optional) + :return: None + """ + whitelist = whitelist or _CONF['exchange']['pair_whitelist'] + + sanitized_whitelist = [] + health = exchange.get_wallet_health() + for status in health: + pair = '{}_{}'.format(_CONF['stake_currency'], status['Currency']) + if pair not in whitelist: + continue + if status['IsActive']: + sanitized_whitelist.append(pair) + else: + logger.info( + 'Ignoring %s from whitelist (reason: %s).', + pair, status.get('Notice') or 'wallet is not active' + ) + if _CONF['exchange']['pair_whitelist'] != sanitized_whitelist: + logger.debug('Using refreshed pair whitelist: %s ...', sanitized_whitelist) + _CONF['exchange']['pair_whitelist'] = sanitized_whitelist + + +def _process(dynamic_whitelist: Optional[bool] = False) -> bool: """ Queries the persistence layer for open trades and handles them, otherwise a new trade is created. + :param: dynamic_whitelist: True is a dynamic whitelist should be generated (optional) :return: True if a trade has been created or closed, False otherwise """ state_changed = False try: + # Refresh whitelist based on wallet maintenance + refresh_whitelist( + gen_pair_whitelist(_CONF['stake_currency']) if dynamic_whitelist else None + ) # Query trades from persistence layer trades = Trade.query.filter(Trade.is_open.is_(True)).all() if len(trades) < _CONF['max_open_trades']: @@ -259,9 +290,7 @@ def gen_pair_whitelist(base_currency: str, topn: int = 20, key: str = 'BaseVolum key=lambda s: s[key], reverse=True ) - pairs = [s['MarketName'].replace('-', '_') for s in summaries[:topn]] - logger.debug('Generated pair whitelist: %s ...', pairs) - return pairs + return [s['MarketName'].replace('-', '_') for s in summaries[:topn]] def cleanup(*args, **kwargs) -> None: @@ -320,9 +349,11 @@ def main(): if new_state == State.STOPPED: time.sleep(1) elif new_state == State.RUNNING: - if args.dynamic_whitelist: - _CONF['exchange']['pair_whitelist'] = gen_pair_whitelist(_CONF['stake_currency']) - throttle(_process, min_secs=_CONF['internals'].get('process_throttle_secs', 10)) + throttle( + _process, + min_secs=_CONF['internals'].get('process_throttle_secs', 10), + dynamic_whitelist=args.dynamic_whitelist, + ) old_state = new_state diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index 25d77d688..25273c546 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -37,7 +37,8 @@ def default_conf(): "BTC_ETH", "BTC_TKN", "BTC_TRST", - "BTC_SWT" + "BTC_SWT", + "BTC_BCC" ] }, "telegram": { @@ -90,6 +91,36 @@ def ticker(): }) +@pytest.fixture +def health(): + return MagicMock(return_value=[{ + 'Currency': 'BTC', + 'IsActive': True, + 'LastChecked': '2017-11-13T20:15:00.00', + 'Notice': None + }, { + 'Currency': 'ETH', + 'IsActive': True, + 'LastChecked': '2017-11-13T20:15:00.00', + 'Notice': None + }, { + 'Currency': 'TRST', + 'IsActive': True, + 'LastChecked': '2017-11-13T20:15:00.00', + 'Notice': None + }, { + 'Currency': 'SWT', + 'IsActive': True, + 'LastChecked': '2017-11-13T20:15:00.00', + 'Notice': None + }, { + 'Currency': 'BCC', + 'IsActive': False, + 'LastChecked': '2017-11-13T20:15:00.00', + 'Notice': None + }]) + + @pytest.fixture def limit_buy_order(): return { diff --git a/freqtrade/tests/test_exchange.py b/freqtrade/tests/test_exchange.py index d4d4e2588..309ff4a5e 100644 --- a/freqtrade/tests/test_exchange.py +++ b/freqtrade/tests/test_exchange.py @@ -8,7 +8,9 @@ from freqtrade.exchange import validate_pairs def test_validate_pairs(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', 'BTC_BCC', + ]) mocker.patch('freqtrade.exchange._API', api_mock) mocker.patch.dict('freqtrade.exchange._CONF', default_conf) validate_pairs(default_conf['exchange']['pair_whitelist']) diff --git a/freqtrade/tests/test_main.py b/freqtrade/tests/test_main.py index f114b4dde..6314a919c 100644 --- a/freqtrade/tests/test_main.py +++ b/freqtrade/tests/test_main.py @@ -13,13 +13,14 @@ from freqtrade.misc import get_state, State from freqtrade.persistence import Trade -def test_process_trade_creation(default_conf, ticker, mocker): +def test_process_trade_creation(default_conf, ticker, health, mocker): mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock()) mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True) mocker.patch.multiple('freqtrade.main.exchange', validate_pairs=MagicMock(), get_ticker=ticker, + get_wallet_health=health, buy=MagicMock(return_value='mocked_limit_buy')) init(default_conf, create_engine('sqlite://')) @@ -41,7 +42,7 @@ def test_process_trade_creation(default_conf, ticker, mocker): assert trade.amount == 0.6864067381401302 -def test_process_exchange_failures(default_conf, ticker, mocker): +def test_process_exchange_failures(default_conf, ticker, health, mocker): mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock()) mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True) @@ -49,6 +50,7 @@ def test_process_exchange_failures(default_conf, ticker, mocker): mocker.patch.multiple('freqtrade.main.exchange', validate_pairs=MagicMock(), get_ticker=ticker, + get_wallet_health=health, buy=MagicMock(side_effect=requests.exceptions.RequestException)) init(default_conf, create_engine('sqlite://')) result = _process() @@ -56,7 +58,7 @@ def test_process_exchange_failures(default_conf, ticker, mocker): assert sleep_mock.has_calls() -def test_process_runtime_error(default_conf, ticker, mocker): +def test_process_runtime_error(default_conf, ticker, health, mocker): msg_mock = MagicMock() mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=msg_mock) @@ -64,6 +66,7 @@ def test_process_runtime_error(default_conf, ticker, mocker): mocker.patch.multiple('freqtrade.main.exchange', validate_pairs=MagicMock(), get_ticker=ticker, + get_wallet_health=health, buy=MagicMock(side_effect=RuntimeError)) init(default_conf, create_engine('sqlite://')) assert get_state() == State.RUNNING @@ -74,13 +77,14 @@ def test_process_runtime_error(default_conf, ticker, mocker): assert 'RuntimeError' in msg_mock.call_args_list[-1][0][0] -def test_process_trade_handling(default_conf, ticker, limit_buy_order, mocker): +def test_process_trade_handling(default_conf, ticker, limit_buy_order, health, mocker): mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock()) mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True) mocker.patch.multiple('freqtrade.main.exchange', validate_pairs=MagicMock(), get_ticker=ticker, + get_wallet_health=health, buy=MagicMock(return_value='mocked_limit_buy'), get_order=MagicMock(return_value=limit_buy_order)) init(default_conf, create_engine('sqlite://'))