use normal program flow to handle interrupts

This commit is contained in:
gcarq 2017-11-20 22:15:19 +01:00
parent 1931d31147
commit 55a69e4a45
7 changed files with 78 additions and 60 deletions

View File

@ -1,4 +1,16 @@
""" FreqTrade bot """ """ FreqTrade bot """
__version__ = '0.14.3' __version__ = '0.14.3'
from . import main
class DependencyException(BaseException):
"""
Indicates that a assumed dependency is not met.
This could happen when there is currently not enough money on the account.
"""
class OperationalException(BaseException):
"""
Requires manual intervention.
This happens when an exchange returns an unexpected error during runtime.
"""

View File

@ -11,6 +11,7 @@ from cachetools import cached, TTLCache
from freqtrade.exchange.bittrex import Bittrex from freqtrade.exchange.bittrex import Bittrex
from freqtrade.exchange.interface import Exchange from freqtrade.exchange.interface import Exchange
from freqtrade import OperationalException
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -51,7 +52,7 @@ def init(config: dict) -> None:
try: try:
exchange_class = Exchanges[name.upper()].value exchange_class = Exchanges[name.upper()].value
except KeyError: except KeyError:
raise RuntimeError('Exchange {} is not supported'.format(name)) raise OperationalException('Exchange {} is not supported'.format(name))
_API = exchange_class(exchange_config) _API = exchange_class(exchange_config)
@ -62,7 +63,7 @@ def init(config: dict) -> None:
def validate_pairs(pairs: List[str]) -> None: def validate_pairs(pairs: List[str]) -> None:
""" """
Checks if all given pairs are tradable on the current exchange. Checks if all given pairs are tradable on the current exchange.
Raises RuntimeError if one pair is not available. Raises OperationalException if one pair is not available.
:param pairs: list of pairs :param pairs: list of pairs
:return: None :return: None
""" """
@ -75,11 +76,12 @@ def validate_pairs(pairs: List[str]) -> None:
stake_cur = _CONF['stake_currency'] stake_cur = _CONF['stake_currency']
for pair in pairs: for pair in pairs:
if not pair.startswith(stake_cur): if not pair.startswith(stake_cur):
raise RuntimeError( raise OperationalException(
'Pair {} not compatible with stake_currency: {}'.format(pair, stake_cur) 'Pair {} not compatible with stake_currency: {}'.format(pair, stake_cur)
) )
if pair not in markets: if pair not in markets:
raise RuntimeError('Pair {} is not available at {}'.format(pair, _API.name.lower())) raise OperationalException(
'Pair {} is not available at {}'.format(pair, _API.name.lower()))
def buy(pair: str, rate: float, amount: float) -> str: def buy(pair: str, rate: float, amount: float) -> str:

View File

@ -5,6 +5,7 @@ from bittrex.bittrex import Bittrex as _Bittrex, API_V2_0, API_V1_1
from requests.exceptions import ContentDecodingError from requests.exceptions import ContentDecodingError
from freqtrade.exchange.interface import Exchange from freqtrade.exchange.interface import Exchange
from freqtrade import OperationalException
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -46,7 +47,7 @@ class Bittrex(Exchange):
def buy(self, pair: str, rate: float, amount: float) -> str: def buy(self, pair: str, rate: float, amount: float) -> str:
data = _API.buy_limit(pair.replace('_', '-'), amount, rate) data = _API.buy_limit(pair.replace('_', '-'), amount, rate)
if not data['success']: if not data['success']:
raise RuntimeError('{message} params=({pair}, {rate}, {amount})'.format( raise OperationalException('{message} params=({pair}, {rate}, {amount})'.format(
message=data['message'], message=data['message'],
pair=pair, pair=pair,
rate=rate, rate=rate,
@ -56,7 +57,7 @@ class Bittrex(Exchange):
def sell(self, pair: str, rate: float, amount: float) -> str: def sell(self, pair: str, rate: float, amount: float) -> str:
data = _API.sell_limit(pair.replace('_', '-'), amount, rate) data = _API.sell_limit(pair.replace('_', '-'), amount, rate)
if not data['success']: if not data['success']:
raise RuntimeError('{message} params=({pair}, {rate}, {amount})'.format( raise OperationalException('{message} params=({pair}, {rate}, {amount})'.format(
message=data['message'], message=data['message'],
pair=pair, pair=pair,
rate=rate, rate=rate,
@ -66,7 +67,7 @@ class Bittrex(Exchange):
def get_balance(self, currency: str) -> float: def get_balance(self, currency: str) -> float:
data = _API.get_balance(currency) data = _API.get_balance(currency)
if not data['success']: if not data['success']:
raise RuntimeError('{message} params=({currency})'.format( raise OperationalException('{message} params=({currency})'.format(
message=data['message'], message=data['message'],
currency=currency)) currency=currency))
return float(data['result']['Balance'] or 0.0) return float(data['result']['Balance'] or 0.0)
@ -74,13 +75,13 @@ class Bittrex(Exchange):
def get_balances(self): def get_balances(self):
data = _API.get_balances() data = _API.get_balances()
if not data['success']: if not data['success']:
raise RuntimeError('{message}'.format(message=data['message'])) raise OperationalException('{message}'.format(message=data['message']))
return data['result'] return data['result']
def get_ticker(self, pair: str) -> dict: def get_ticker(self, pair: str) -> dict:
data = _API.get_ticker(pair.replace('_', '-')) data = _API.get_ticker(pair.replace('_', '-'))
if not data['success']: if not data['success']:
raise RuntimeError('{message} params=({pair})'.format( raise OperationalException('{message} params=({pair})'.format(
message=data['message'], message=data['message'],
pair=pair)) pair=pair))
@ -121,7 +122,7 @@ class Bittrex(Exchange):
pair=pair)) pair=pair))
if not data['success']: if not data['success']:
raise RuntimeError('{message} params=({pair})'.format( raise OperationalException('{message} params=({pair})'.format(
message=data['message'], message=data['message'],
pair=pair)) pair=pair))
@ -130,7 +131,7 @@ class Bittrex(Exchange):
def get_order(self, order_id: str) -> Dict: def get_order(self, order_id: str) -> Dict:
data = _API.get_order(order_id) data = _API.get_order(order_id)
if not data['success']: if not data['success']:
raise RuntimeError('{message} params=({order_id})'.format( raise OperationalException('{message} params=({order_id})'.format(
message=data['message'], message=data['message'],
order_id=order_id)) order_id=order_id))
data = data['result'] data = data['result']
@ -148,7 +149,7 @@ class Bittrex(Exchange):
def cancel_order(self, order_id: str) -> None: def cancel_order(self, order_id: str) -> None:
data = _API.cancel(order_id) data = _API.cancel(order_id)
if not data['success']: if not data['success']:
raise RuntimeError('{message} params=({order_id})'.format( raise OperationalException('{message} params=({order_id})'.format(
message=data['message'], message=data['message'],
order_id=order_id)) order_id=order_id))
@ -158,19 +159,19 @@ class Bittrex(Exchange):
def get_markets(self) -> List[str]: def get_markets(self) -> List[str]:
data = _API.get_markets() data = _API.get_markets()
if not data['success']: if not data['success']:
raise RuntimeError('{message}'.format(message=data['message'])) raise OperationalException('{message}'.format(message=data['message']))
return [m['MarketName'].replace('-', '_') for m in data['result']] return [m['MarketName'].replace('-', '_') for m in data['result']]
def get_market_summaries(self) -> List[Dict]: def get_market_summaries(self) -> List[Dict]:
data = _API.get_market_summaries() data = _API.get_market_summaries()
if not data['success']: if not data['success']:
raise RuntimeError('{message}'.format(message=data['message'])) raise OperationalException('{message}'.format(message=data['message']))
return data['result'] return data['result']
def get_wallet_health(self) -> List[Dict]: def get_wallet_health(self) -> List[Dict]:
data = _API_V2.get_wallet_health() data = _API_V2.get_wallet_health()
if not data['success']: if not data['success']:
raise RuntimeError('{message}'.format(message=data['message'])) raise OperationalException('{message}'.format(message=data['message']))
return [{ return [{
'Currency': entry['Health']['Currency'], 'Currency': entry['Health']['Currency'],
'IsActive': entry['Health']['IsActive'], 'IsActive': entry['Health']['IsActive'],

View File

@ -6,16 +6,16 @@ import sys
import time import time
import traceback import traceback
from datetime import datetime from datetime import datetime
from signal import signal, SIGINT, SIGABRT, SIGTERM
from typing import Dict, Optional, List from typing import Dict, Optional, List
import requests import requests
from cachetools import cached, TTLCache from cachetools import cached, TTLCache
from freqtrade import __version__, exchange, persistence, rpc from freqtrade import __version__, exchange, persistence, rpc, DependencyException, \
OperationalException
from freqtrade.analyze import get_signal, SignalType from freqtrade.analyze import get_signal, SignalType
from freqtrade.misc import State, get_state, update_state, parse_args, throttle, \ from freqtrade.misc import State, get_state, update_state, parse_args, throttle, \
load_config, FreqtradeException load_config
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
logger = logging.getLogger('freqtrade') logger = logging.getLogger('freqtrade')
@ -76,7 +76,7 @@ def _process(dynamic_whitelist: Optional[bool] = False) -> bool:
'Checked all whitelisted currencies. ' 'Checked all whitelisted currencies. '
'Found no suitable entry positions for buying. Will keep looking ...' 'Found no suitable entry positions for buying. Will keep looking ...'
) )
except FreqtradeException as e: except DependencyException as e:
logger.warning('Unable to create trade: %s', e) logger.warning('Unable to create trade: %s', e)
for trade in trades: for trade in trades:
@ -97,12 +97,12 @@ def _process(dynamic_whitelist: Optional[bool] = False) -> bool:
error error
) )
time.sleep(30) time.sleep(30)
except RuntimeError: except OperationalException:
rpc.send_msg('*Status:* Got RuntimeError:\n```\n{traceback}```{hint}'.format( rpc.send_msg('*Status:* Got OperationalException:\n```\n{traceback}```{hint}'.format(
traceback=traceback.format_exc(), traceback=traceback.format_exc(),
hint='Issue `/start` if you think it is safe to restart.' hint='Issue `/start` if you think it is safe to restart.'
)) ))
logger.exception('Got RuntimeError. Stopping trader ...') logger.exception('Got OperationalException. Stopping trader ...')
update_state(State.STOPPED) update_state(State.STOPPED)
return state_changed return state_changed
@ -185,7 +185,7 @@ def create_trade(stake_amount: float) -> Optional[Trade]:
whitelist = copy.deepcopy(_CONF['exchange']['pair_whitelist']) whitelist = copy.deepcopy(_CONF['exchange']['pair_whitelist'])
# Check if stake_amount is fulfilled # Check if stake_amount is fulfilled
if exchange.get_balance(_CONF['stake_currency']) < stake_amount: if exchange.get_balance(_CONF['stake_currency']) < stake_amount:
raise FreqtradeException( raise DependencyException(
'stake amount is not fulfilled (currency={})'.format(_CONF['stake_currency']) 'stake amount is not fulfilled (currency={})'.format(_CONF['stake_currency'])
) )
@ -195,7 +195,7 @@ def create_trade(stake_amount: float) -> Optional[Trade]:
whitelist.remove(trade.pair) whitelist.remove(trade.pair)
logger.debug('Ignoring %s in pair whitelist', trade.pair) logger.debug('Ignoring %s in pair whitelist', trade.pair)
if not whitelist: if not whitelist:
raise FreqtradeException('No pair in whitelist') raise DependencyException('No pair in whitelist')
# Pick pair based on StochRSI buy signals # Pick pair based on StochRSI buy signals
for _pair in whitelist: for _pair in whitelist:
@ -248,10 +248,6 @@ def init(config: dict, db_url: Optional[str] = None) -> None:
else: else:
update_state(State.STOPPED) update_state(State.STOPPED)
# Register signal handlers
for sig in (SIGINT, SIGTERM, SIGABRT):
signal(sig, cleanup)
@cached(TTLCache(maxsize=1, ttl=1800)) @cached(TTLCache(maxsize=1, ttl=1800))
def gen_pair_whitelist(base_currency: str, topn: int = 20, key: str = 'BaseVolume') -> List[str]: def gen_pair_whitelist(base_currency: str, topn: int = 20, key: str = 'BaseVolume') -> List[str]:
@ -270,7 +266,7 @@ def gen_pair_whitelist(base_currency: str, topn: int = 20, key: str = 'BaseVolum
return [s['MarketName'].replace('-', '_') for s in summaries[:topn]] return [s['MarketName'].replace('-', '_') for s in summaries[:topn]]
def cleanup(*args, **kwargs) -> None: def cleanup() -> None:
""" """
Cleanup the application state und finish all pending tasks Cleanup the application state und finish all pending tasks
:return: None :return: None
@ -283,7 +279,7 @@ def cleanup(*args, **kwargs) -> None:
exit(0) exit(0)
def main(): def main() -> None:
""" """
Loads and validates the config and handles the main loop Loads and validates the config and handles the main loop
:return: None :return: None
@ -311,24 +307,33 @@ def main():
# Initialize all modules and start main loop # Initialize all modules and start main loop
if args.dynamic_whitelist: if args.dynamic_whitelist:
logger.info('Using dynamically generated whitelist. (--dynamic-whitelist detected)') logger.info('Using dynamically generated whitelist. (--dynamic-whitelist detected)')
init(_CONF)
old_state = None
while True:
new_state = get_state()
# Log state transition
if new_state != old_state:
rpc.send_msg('*Status:* `{}`'.format(new_state.name.lower()))
logger.info('Changing state to: %s', new_state.name)
if new_state == State.STOPPED: try:
time.sleep(1) init(_CONF)
elif new_state == State.RUNNING: old_state = None
throttle( while True:
_process, new_state = get_state()
min_secs=_CONF['internals'].get('process_throttle_secs', 10), # Log state transition
dynamic_whitelist=args.dynamic_whitelist, if new_state != old_state:
) rpc.send_msg('*Status:* `{}`'.format(new_state.name.lower()))
old_state = new_state logger.info('Changing state to: %s', new_state.name)
if new_state == State.STOPPED:
time.sleep(1)
elif new_state == State.RUNNING:
throttle(
_process,
min_secs=_CONF['internals'].get('process_throttle_secs', 10),
dynamic_whitelist=args.dynamic_whitelist,
)
old_state = new_state
except RuntimeError:
logger.exception('Got fatal exception!')
except KeyboardInterrupt:
logger.info('Got SIGINT, aborting ...')
finally:
cleanup()
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -15,10 +15,6 @@ from freqtrade import __version__
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class FreqtradeException(BaseException):
pass
class State(enum.Enum): class State(enum.Enum):
RUNNING = 0 RUNNING = 0
STOPPED = 1 STOPPED = 1

View File

@ -4,6 +4,7 @@ from unittest.mock import MagicMock
import pytest import pytest
from freqtrade.exchange import validate_pairs from freqtrade.exchange import validate_pairs
from freqtrade.misc import OperationalException
def test_validate_pairs(default_conf, mocker): def test_validate_pairs(default_conf, mocker):
@ -21,7 +22,7 @@ def test_validate_pairs_not_available(default_conf, mocker):
api_mock.get_markets = MagicMock(return_value=[]) api_mock.get_markets = MagicMock(return_value=[])
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)
with pytest.raises(RuntimeError, match=r'not available'): with pytest.raises(OperationalException, match=r'not available'):
validate_pairs(default_conf['exchange']['pair_whitelist']) validate_pairs(default_conf['exchange']['pair_whitelist'])
@ -31,5 +32,5 @@ def test_validate_pairs_not_compatible(default_conf, mocker):
default_conf['stake_currency'] = 'ETH' default_conf['stake_currency'] = 'ETH'
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)
with pytest.raises(RuntimeError, match=r'not compatible'): with pytest.raises(OperationalException, match=r'not compatible'):
validate_pairs(default_conf['exchange']['pair_whitelist']) validate_pairs(default_conf['exchange']['pair_whitelist'])

View File

@ -6,11 +6,12 @@ import pytest
import requests import requests
from sqlalchemy import create_engine from sqlalchemy import create_engine
from freqtrade import DependencyException, OperationalException
from freqtrade.exchange import Exchanges from freqtrade.exchange import Exchanges
from freqtrade.analyze import SignalType from freqtrade.analyze import SignalType
from freqtrade.main import create_trade, handle_trade, init, \ from freqtrade.main import create_trade, handle_trade, init, \
get_target_bid, _process get_target_bid, _process
from freqtrade.misc import get_state, State, FreqtradeException from freqtrade.misc import get_state, State
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
@ -59,7 +60,7 @@ def test_process_exchange_failures(default_conf, ticker, health, mocker):
assert sleep_mock.has_calls() assert sleep_mock.has_calls()
def test_process_runtime_error(default_conf, ticker, health, mocker): def test_process_operational_exception(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.rpc', init=MagicMock(), send_msg=msg_mock) mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=msg_mock)
@ -68,14 +69,14 @@ def test_process_runtime_error(default_conf, ticker, health, mocker):
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
get_ticker=ticker, get_ticker=ticker,
get_wallet_health=health, get_wallet_health=health,
buy=MagicMock(side_effect=RuntimeError)) buy=MagicMock(side_effect=OperationalException))
init(default_conf, create_engine('sqlite://')) init(default_conf, create_engine('sqlite://'))
assert get_state() == State.RUNNING assert get_state() == State.RUNNING
result = _process() result = _process()
assert result is False assert result is False
assert get_state() == State.STOPPED assert get_state() == State.STOPPED
assert 'RuntimeError' in msg_mock.call_args_list[-1][0][0] assert 'OperationalException' in msg_mock.call_args_list[-1][0][0]
def test_process_trade_handling(default_conf, ticker, limit_buy_order, health, mocker): def test_process_trade_handling(default_conf, ticker, limit_buy_order, health, mocker):
@ -141,7 +142,7 @@ def test_create_trade_no_stake_amount(default_conf, ticker, mocker):
get_ticker=ticker, get_ticker=ticker,
buy=MagicMock(return_value='mocked_limit_buy'), buy=MagicMock(return_value='mocked_limit_buy'),
get_balance=MagicMock(return_value=default_conf['stake_amount'] * 0.5)) get_balance=MagicMock(return_value=default_conf['stake_amount'] * 0.5))
with pytest.raises(FreqtradeException, match=r'.*stake amount.*'): with pytest.raises(DependencyException, match=r'.*stake amount.*'):
create_trade(default_conf['stake_amount']) create_trade(default_conf['stake_amount'])
@ -154,7 +155,7 @@ def test_create_trade_no_pairs(default_conf, ticker, mocker):
get_ticker=ticker, get_ticker=ticker,
buy=MagicMock(return_value='mocked_limit_buy')) buy=MagicMock(return_value='mocked_limit_buy'))
with pytest.raises(FreqtradeException, match=r'.*No pair in whitelist.*'): with pytest.raises(DependencyException, match=r'.*No pair in whitelist.*'):
conf = copy.deepcopy(default_conf) conf = copy.deepcopy(default_conf)
conf['exchange']['pair_whitelist'] = [] conf['exchange']['pair_whitelist'] = []
mocker.patch.dict('freqtrade.main._CONF', conf) mocker.patch.dict('freqtrade.main._CONF', conf)