use normal program flow to handle interrupts
This commit is contained in:
		| @@ -1,4 +1,16 @@ | ||||
| """ FreqTrade bot """ | ||||
| __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. | ||||
|     """ | ||||
|   | ||||
| @@ -11,6 +11,7 @@ from cachetools import cached, TTLCache | ||||
|  | ||||
| from freqtrade.exchange.bittrex import Bittrex | ||||
| from freqtrade.exchange.interface import Exchange | ||||
| from freqtrade import OperationalException | ||||
|  | ||||
| logger = logging.getLogger(__name__) | ||||
|  | ||||
| @@ -51,7 +52,7 @@ def init(config: dict) -> None: | ||||
|     try: | ||||
|         exchange_class = Exchanges[name.upper()].value | ||||
|     except KeyError: | ||||
|         raise RuntimeError('Exchange {} is not supported'.format(name)) | ||||
|         raise OperationalException('Exchange {} is not supported'.format(name)) | ||||
|  | ||||
|     _API = exchange_class(exchange_config) | ||||
|  | ||||
| @@ -62,7 +63,7 @@ def init(config: dict) -> None: | ||||
| def validate_pairs(pairs: List[str]) -> None: | ||||
|     """ | ||||
|     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 | ||||
|     :return: None | ||||
|     """ | ||||
| @@ -75,11 +76,12 @@ def validate_pairs(pairs: List[str]) -> None: | ||||
|     stake_cur = _CONF['stake_currency'] | ||||
|     for pair in pairs: | ||||
|         if not pair.startswith(stake_cur): | ||||
|             raise RuntimeError( | ||||
|             raise OperationalException( | ||||
|                 'Pair {} not compatible with stake_currency: {}'.format(pair, stake_cur) | ||||
|             ) | ||||
|         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: | ||||
|   | ||||
| @@ -5,6 +5,7 @@ from bittrex.bittrex import Bittrex as _Bittrex, API_V2_0, API_V1_1 | ||||
| from requests.exceptions import ContentDecodingError | ||||
|  | ||||
| from freqtrade.exchange.interface import Exchange | ||||
| from freqtrade import OperationalException | ||||
|  | ||||
| logger = logging.getLogger(__name__) | ||||
|  | ||||
| @@ -46,7 +47,7 @@ class Bittrex(Exchange): | ||||
|     def buy(self, pair: str, rate: float, amount: float) -> str: | ||||
|         data = _API.buy_limit(pair.replace('_', '-'), amount, rate) | ||||
|         if not data['success']: | ||||
|             raise RuntimeError('{message} params=({pair}, {rate}, {amount})'.format( | ||||
|             raise OperationalException('{message} params=({pair}, {rate}, {amount})'.format( | ||||
|                 message=data['message'], | ||||
|                 pair=pair, | ||||
|                 rate=rate, | ||||
| @@ -56,7 +57,7 @@ class Bittrex(Exchange): | ||||
|     def sell(self, pair: str, rate: float, amount: float) -> str: | ||||
|         data = _API.sell_limit(pair.replace('_', '-'), amount, rate) | ||||
|         if not data['success']: | ||||
|             raise RuntimeError('{message} params=({pair}, {rate}, {amount})'.format( | ||||
|             raise OperationalException('{message} params=({pair}, {rate}, {amount})'.format( | ||||
|                 message=data['message'], | ||||
|                 pair=pair, | ||||
|                 rate=rate, | ||||
| @@ -66,7 +67,7 @@ class Bittrex(Exchange): | ||||
|     def get_balance(self, currency: str) -> float: | ||||
|         data = _API.get_balance(currency) | ||||
|         if not data['success']: | ||||
|             raise RuntimeError('{message} params=({currency})'.format( | ||||
|             raise OperationalException('{message} params=({currency})'.format( | ||||
|                 message=data['message'], | ||||
|                 currency=currency)) | ||||
|         return float(data['result']['Balance'] or 0.0) | ||||
| @@ -74,13 +75,13 @@ class Bittrex(Exchange): | ||||
|     def get_balances(self): | ||||
|         data = _API.get_balances() | ||||
|         if not data['success']: | ||||
|             raise RuntimeError('{message}'.format(message=data['message'])) | ||||
|             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']: | ||||
|             raise RuntimeError('{message} params=({pair})'.format( | ||||
|             raise OperationalException('{message} params=({pair})'.format( | ||||
|                 message=data['message'], | ||||
|                 pair=pair)) | ||||
|  | ||||
| @@ -121,7 +122,7 @@ class Bittrex(Exchange): | ||||
|                         pair=pair)) | ||||
|  | ||||
|         if not data['success']: | ||||
|             raise RuntimeError('{message} params=({pair})'.format( | ||||
|             raise OperationalException('{message} params=({pair})'.format( | ||||
|                 message=data['message'], | ||||
|                 pair=pair)) | ||||
|  | ||||
| @@ -130,7 +131,7 @@ class Bittrex(Exchange): | ||||
|     def get_order(self, order_id: str) -> Dict: | ||||
|         data = _API.get_order(order_id) | ||||
|         if not data['success']: | ||||
|             raise RuntimeError('{message} params=({order_id})'.format( | ||||
|             raise OperationalException('{message} params=({order_id})'.format( | ||||
|                 message=data['message'], | ||||
|                 order_id=order_id)) | ||||
|         data = data['result'] | ||||
| @@ -148,7 +149,7 @@ class Bittrex(Exchange): | ||||
|     def cancel_order(self, order_id: str) -> None: | ||||
|         data = _API.cancel(order_id) | ||||
|         if not data['success']: | ||||
|             raise RuntimeError('{message} params=({order_id})'.format( | ||||
|             raise OperationalException('{message} params=({order_id})'.format( | ||||
|                 message=data['message'], | ||||
|                 order_id=order_id)) | ||||
|  | ||||
| @@ -158,19 +159,19 @@ class Bittrex(Exchange): | ||||
|     def get_markets(self) -> List[str]: | ||||
|         data = _API.get_markets() | ||||
|         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']] | ||||
|  | ||||
|     def get_market_summaries(self) -> List[Dict]: | ||||
|         data = _API.get_market_summaries() | ||||
|         if not data['success']: | ||||
|             raise RuntimeError('{message}'.format(message=data['message'])) | ||||
|             raise OperationalException('{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'])) | ||||
|             raise OperationalException('{message}'.format(message=data['message'])) | ||||
|         return [{ | ||||
|             'Currency': entry['Health']['Currency'], | ||||
|             'IsActive': entry['Health']['IsActive'], | ||||
|   | ||||
| @@ -6,16 +6,16 @@ import sys | ||||
| import time | ||||
| import traceback | ||||
| from datetime import datetime | ||||
| from signal import signal, SIGINT, SIGABRT, SIGTERM | ||||
| from typing import Dict, Optional, List | ||||
|  | ||||
| import requests | ||||
| 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.misc import State, get_state, update_state, parse_args, throttle, \ | ||||
|     load_config, FreqtradeException | ||||
|     load_config | ||||
| from freqtrade.persistence import Trade | ||||
|  | ||||
| logger = logging.getLogger('freqtrade') | ||||
| @@ -76,7 +76,7 @@ def _process(dynamic_whitelist: Optional[bool] = False) -> bool: | ||||
|                         'Checked all whitelisted currencies. ' | ||||
|                         '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) | ||||
|  | ||||
|         for trade in trades: | ||||
| @@ -97,12 +97,12 @@ def _process(dynamic_whitelist: Optional[bool] = False) -> bool: | ||||
|             error | ||||
|         ) | ||||
|         time.sleep(30) | ||||
|     except RuntimeError: | ||||
|         rpc.send_msg('*Status:* Got RuntimeError:\n```\n{traceback}```{hint}'.format( | ||||
|     except OperationalException: | ||||
|         rpc.send_msg('*Status:* Got OperationalException:\n```\n{traceback}```{hint}'.format( | ||||
|             traceback=traceback.format_exc(), | ||||
|             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) | ||||
|     return state_changed | ||||
|  | ||||
| @@ -185,7 +185,7 @@ def create_trade(stake_amount: float) -> Optional[Trade]: | ||||
|     whitelist = copy.deepcopy(_CONF['exchange']['pair_whitelist']) | ||||
|     # Check if stake_amount is fulfilled | ||||
|     if exchange.get_balance(_CONF['stake_currency']) < stake_amount: | ||||
|         raise FreqtradeException( | ||||
|         raise DependencyException( | ||||
|             '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) | ||||
|             logger.debug('Ignoring %s in pair whitelist', trade.pair) | ||||
|     if not whitelist: | ||||
|         raise FreqtradeException('No pair in whitelist') | ||||
|         raise DependencyException('No pair in whitelist') | ||||
|  | ||||
|     # Pick pair based on StochRSI buy signals | ||||
|     for _pair in whitelist: | ||||
| @@ -248,10 +248,6 @@ def init(config: dict, db_url: Optional[str] = None) -> None: | ||||
|     else: | ||||
|         update_state(State.STOPPED) | ||||
|  | ||||
|     # Register signal handlers | ||||
|     for sig in (SIGINT, SIGTERM, SIGABRT): | ||||
|         signal(sig, cleanup) | ||||
|  | ||||
|  | ||||
| @cached(TTLCache(maxsize=1, ttl=1800)) | ||||
| 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]] | ||||
|  | ||||
|  | ||||
| def cleanup(*args, **kwargs) -> None: | ||||
| def cleanup() -> None: | ||||
|     """ | ||||
|     Cleanup the application state und finish all pending tasks | ||||
|     :return: None | ||||
| @@ -283,7 +279,7 @@ def cleanup(*args, **kwargs) -> None: | ||||
|     exit(0) | ||||
|  | ||||
|  | ||||
| def main(): | ||||
| def main() -> None: | ||||
|     """ | ||||
|     Loads and validates the config and handles the main loop | ||||
|     :return: None | ||||
| @@ -311,24 +307,33 @@ def main(): | ||||
|     # Initialize all modules and start main loop | ||||
|     if args.dynamic_whitelist: | ||||
|         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: | ||||
|             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 | ||||
|     try: | ||||
|         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: | ||||
|                 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__': | ||||
|   | ||||
| @@ -15,10 +15,6 @@ from freqtrade import __version__ | ||||
| logger = logging.getLogger(__name__) | ||||
|  | ||||
|  | ||||
| class FreqtradeException(BaseException): | ||||
|     pass | ||||
|  | ||||
|  | ||||
| class State(enum.Enum): | ||||
|     RUNNING = 0 | ||||
|     STOPPED = 1 | ||||
|   | ||||
| @@ -4,6 +4,7 @@ from unittest.mock import MagicMock | ||||
| import pytest | ||||
|  | ||||
| from freqtrade.exchange import validate_pairs | ||||
| from freqtrade.misc import OperationalException | ||||
|  | ||||
|  | ||||
| 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=[]) | ||||
|     mocker.patch('freqtrade.exchange._API', api_mock) | ||||
|     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']) | ||||
|  | ||||
|  | ||||
| @@ -31,5 +32,5 @@ def test_validate_pairs_not_compatible(default_conf, mocker): | ||||
|     default_conf['stake_currency'] = 'ETH' | ||||
|     mocker.patch('freqtrade.exchange._API', api_mock) | ||||
|     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']) | ||||
|   | ||||
| @@ -6,11 +6,12 @@ import pytest | ||||
| import requests | ||||
| from sqlalchemy import create_engine | ||||
|  | ||||
| from freqtrade import DependencyException, OperationalException | ||||
| from freqtrade.exchange import Exchanges | ||||
| from freqtrade.analyze import SignalType | ||||
| from freqtrade.main import create_trade, handle_trade, init, \ | ||||
|     get_target_bid, _process | ||||
| from freqtrade.misc import get_state, State, FreqtradeException | ||||
| from freqtrade.misc import get_state, State | ||||
| from freqtrade.persistence import Trade | ||||
|  | ||||
|  | ||||
| @@ -59,7 +60,7 @@ def test_process_exchange_failures(default_conf, ticker, health, mocker): | ||||
|     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() | ||||
|     mocker.patch.dict('freqtrade.main._CONF', default_conf) | ||||
|     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(), | ||||
|                           get_ticker=ticker, | ||||
|                           get_wallet_health=health, | ||||
|                           buy=MagicMock(side_effect=RuntimeError)) | ||||
|                           buy=MagicMock(side_effect=OperationalException)) | ||||
|     init(default_conf, create_engine('sqlite://')) | ||||
|     assert get_state() == State.RUNNING | ||||
|  | ||||
|     result = _process() | ||||
|     assert result is False | ||||
|     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): | ||||
| @@ -141,7 +142,7 @@ def test_create_trade_no_stake_amount(default_conf, ticker, mocker): | ||||
|                           get_ticker=ticker, | ||||
|                           buy=MagicMock(return_value='mocked_limit_buy'), | ||||
|                           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']) | ||||
|  | ||||
|  | ||||
| @@ -154,7 +155,7 @@ def test_create_trade_no_pairs(default_conf, ticker, mocker): | ||||
|                           get_ticker=ticker, | ||||
|                           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['exchange']['pair_whitelist'] = [] | ||||
|         mocker.patch.dict('freqtrade.main._CONF', conf) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user