use normal program flow to handle interrupts
This commit is contained in:
parent
1931d31147
commit
55a69e4a45
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user