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.
This commit is contained in:
gcarq 2017-11-13 21:34:47 +01:00
parent 81f7172c4a
commit dd9cb008fb
7 changed files with 115 additions and 14 deletions

View File

@ -169,3 +169,7 @@ def get_name() -> str:
def get_fee() -> float: def get_fee() -> float:
return _API.fee return _API.fee
def get_wallet_health() -> List[Dict]:
return _API.get_wallet_health()

View File

@ -82,6 +82,10 @@ class Bittrex(Exchange):
raise RuntimeError('{message} params=({pair})'.format( raise RuntimeError('{message} params=({pair})'.format(
message=data['message'], message=data['message'],
pair=pair)) 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 { return {
'bid': float(data['result']['Bid']), 'bid': float(data['result']['Bid']),
'ask': float(data['result']['Ask']), 'ask': float(data['result']['Ask']),
@ -101,7 +105,7 @@ class Bittrex(Exchange):
for prop in ['C', 'V', 'O', 'H', 'L', 'T']: for prop in ['C', 'V', 'O', 'H', 'L', 'T']:
for tick in data['result']: for tick in data['result']:
if prop not in tick.keys(): 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 [] return []
if not data['success']: if not data['success']:
@ -150,3 +154,14 @@ class Bittrex(Exchange):
if not data['success']: if not data['success']:
raise RuntimeError('{message}'.format(message=data['message'])) raise RuntimeError('{message}'.format(message=data['message']))
return data['result'] 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']]

View File

@ -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
},
...
"""

View File

@ -23,14 +23,45 @@ logger = logging.getLogger('freqtrade')
_CONF = {} _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, Queries the persistence layer for open trades and handles them,
otherwise a new trade is created. 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 :return: True if a trade has been created or closed, False otherwise
""" """
state_changed = False state_changed = False
try: 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 # Query trades from persistence layer
trades = Trade.query.filter(Trade.is_open.is_(True)).all() trades = Trade.query.filter(Trade.is_open.is_(True)).all()
if len(trades) < _CONF['max_open_trades']: 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], key=lambda s: s[key],
reverse=True reverse=True
) )
pairs = [s['MarketName'].replace('-', '_') for s in summaries[:topn]] return [s['MarketName'].replace('-', '_') for s in summaries[:topn]]
logger.debug('Generated pair whitelist: %s ...', pairs)
return pairs
def cleanup(*args, **kwargs) -> None: def cleanup(*args, **kwargs) -> None:
@ -320,9 +349,11 @@ def main():
if new_state == State.STOPPED: if new_state == State.STOPPED:
time.sleep(1) time.sleep(1)
elif new_state == State.RUNNING: elif new_state == State.RUNNING:
if args.dynamic_whitelist: throttle(
_CONF['exchange']['pair_whitelist'] = gen_pair_whitelist(_CONF['stake_currency']) _process,
throttle(_process, min_secs=_CONF['internals'].get('process_throttle_secs', 10)) min_secs=_CONF['internals'].get('process_throttle_secs', 10),
dynamic_whitelist=args.dynamic_whitelist,
)
old_state = new_state old_state = new_state

View File

@ -37,7 +37,8 @@ def default_conf():
"BTC_ETH", "BTC_ETH",
"BTC_TKN", "BTC_TKN",
"BTC_TRST", "BTC_TRST",
"BTC_SWT" "BTC_SWT",
"BTC_BCC"
] ]
}, },
"telegram": { "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 @pytest.fixture
def limit_buy_order(): def limit_buy_order():
return { return {

View File

@ -8,7 +8,9 @@ from freqtrade.exchange import validate_pairs
def test_validate_pairs(default_conf, mocker): def test_validate_pairs(default_conf, mocker):
api_mock = MagicMock() 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('freqtrade.exchange._API', api_mock)
mocker.patch.dict('freqtrade.exchange._CONF', default_conf) mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
validate_pairs(default_conf['exchange']['pair_whitelist']) validate_pairs(default_conf['exchange']['pair_whitelist'])

View File

@ -13,13 +13,14 @@ from freqtrade.misc import get_state, State
from freqtrade.persistence import Trade 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.dict('freqtrade.main._CONF', default_conf)
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock()) mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock())
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True) mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
mocker.patch.multiple('freqtrade.main.exchange', mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
get_ticker=ticker, get_ticker=ticker,
get_wallet_health=health,
buy=MagicMock(return_value='mocked_limit_buy')) buy=MagicMock(return_value='mocked_limit_buy'))
init(default_conf, create_engine('sqlite://')) init(default_conf, create_engine('sqlite://'))
@ -41,7 +42,7 @@ def test_process_trade_creation(default_conf, ticker, mocker):
assert trade.amount == 0.6864067381401302 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.dict('freqtrade.main._CONF', default_conf)
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock()) mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock())
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True) 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', mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
get_ticker=ticker, get_ticker=ticker,
get_wallet_health=health,
buy=MagicMock(side_effect=requests.exceptions.RequestException)) buy=MagicMock(side_effect=requests.exceptions.RequestException))
init(default_conf, create_engine('sqlite://')) init(default_conf, create_engine('sqlite://'))
result = _process() result = _process()
@ -56,7 +58,7 @@ def test_process_exchange_failures(default_conf, ticker, mocker):
assert sleep_mock.has_calls() 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() msg_mock = MagicMock()
mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=msg_mock) 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', mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
get_ticker=ticker, get_ticker=ticker,
get_wallet_health=health,
buy=MagicMock(side_effect=RuntimeError)) buy=MagicMock(side_effect=RuntimeError))
init(default_conf, create_engine('sqlite://')) init(default_conf, create_engine('sqlite://'))
assert get_state() == State.RUNNING 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] 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.dict('freqtrade.main._CONF', default_conf)
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock()) mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock())
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True) mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
mocker.patch.multiple('freqtrade.main.exchange', mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
get_ticker=ticker, get_ticker=ticker,
get_wallet_health=health,
buy=MagicMock(return_value='mocked_limit_buy'), buy=MagicMock(return_value='mocked_limit_buy'),
get_order=MagicMock(return_value=limit_buy_order)) get_order=MagicMock(return_value=limit_buy_order))
init(default_conf, create_engine('sqlite://')) init(default_conf, create_engine('sqlite://'))