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:
parent
81f7172c4a
commit
dd9cb008fb
@ -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()
|
||||||
|
@ -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']]
|
||||||
|
@ -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
|
||||||
|
},
|
||||||
|
...
|
||||||
|
"""
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
@ -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'])
|
||||||
|
@ -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://'))
|
||||||
|
Loading…
Reference in New Issue
Block a user