From a4b2f4e4b9aabe10b58a13ac7fe25deb81f37e2c Mon Sep 17 00:00:00 2001 From: gcarq Date: Sat, 9 Sep 2017 00:31:40 +0200 Subject: [PATCH] Fix application state and add new optional config attribute: "initial_state" * Move State handling to misc, to avoid circular imports * Add optional config attribute "initial_state" --- README.md | 6 +- config.json.example | 3 +- main.py | 159 ++++++++++++++++++------------------------ misc.py | 36 +++++++++- rpc/telegram.py | 8 +-- test/test_telegram.py | 13 ++-- 6 files changed, 118 insertions(+), 107 deletions(-) diff --git a/README.md b/README.md index d3d3ae36c..1569eabea 100644 --- a/README.md +++ b/README.md @@ -40,10 +40,14 @@ See the example below: For example value `-0.10` will cause immediate sell if the profit dips below -10% for a given trade. This parameter is optional. +`initial_state` is an optional field that defines the initial application state. +Possible values are `running` or `stopped`. (default=`running`) +If the value is `stopped` the bot has to be started with `/start` first. + The other values should be self-explanatory, if not feel free to raise a github issue. -##### Prerequisites +#### Prerequisites * python3.6 * sqlite * [TA-lib](https://github.com/mrjbq7/ta-lib#dependencies) binaries diff --git a/config.json.example b/config.json.example index a9fc3be79..227d4091e 100644 --- a/config.json.example +++ b/config.json.example @@ -35,5 +35,6 @@ "enabled": true, "token": "token", "chat_id": "chat_id" - } + }, + "initial_state": "running" } \ No newline at end of file diff --git a/main.py b/main.py index 6913fcb97..05126a8d7 100755 --- a/main.py +++ b/main.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -import enum import json import logging import time @@ -8,13 +7,12 @@ from datetime import datetime from typing import Optional from jsonschema import validate -from wrapt import synchronized import exchange import persistence from persistence import Trade from analyze import get_buy_signal -from misc import CONF_SCHEMA +from misc import CONF_SCHEMA, get_state, State, update_state from rpc import telegram logging.basicConfig(level=logging.DEBUG, @@ -26,38 +24,8 @@ __copyright__ = "gcarq 2017" __license__ = "GPLv3" __version__ = "0.8.0" - -class State(enum.Enum): - RUNNING = 0 - PAUSED = 1 - TERMINATE = 2 - - _CONF = {} -# Current application state -_STATE = State.RUNNING - - -@synchronized -def update_state(state: State) -> None: - """ - Updates the application state - :param state: new state - :return: None - """ - global _STATE - _STATE = state - - -@synchronized -def get_state() -> State: - """ - Gets the current application state - :return: - """ - return _STATE - def _process() -> None: """ @@ -65,44 +33,43 @@ def _process() -> None: otherwise a new trade is created. :return: None """ - # Query trades from persistence layer - trades = Trade.query.filter(Trade.is_open.is_(True)).all() - if len(trades) < _CONF['max_open_trades']: - try: - # Create entity and execute trade - trade = create_trade(float(_CONF['stake_amount']), exchange.EXCHANGE) - if trade: - Trade.session.add(trade) + try: + # Query trades from persistence layer + trades = Trade.query.filter(Trade.is_open.is_(True)).all() + if len(trades) < _CONF['max_open_trades']: + try: + # Create entity and execute trade + trade = create_trade(float(_CONF['stake_amount']), exchange.EXCHANGE) + if trade: + Trade.session.add(trade) + else: + logging.info('Got no buy signal...') + except ValueError: + logger.exception('Unable to create trade') + + for trade in trades: + if close_trade_if_fulfilled(trade): + logger.info( + 'No open orders found and trade is fulfilled. Marking %s as closed ...', + trade + ) + Trade.session.flush() + + for trade in filter(lambda t: t.is_open, trades): + # Check if there is already an open order for this trade + orders = exchange.get_open_orders(trade.pair) + orders = [o for o in orders if o['id'] == trade.open_order_id] + if orders: + logger.info('There is an open order for: %s', orders[0]) else: - logging.info('Got no buy signal...') - except ValueError: - logger.exception('Unable to create trade') - - for trade in trades: - if close_trade_if_fulfilled(trade): - logger.info( - 'No open orders found and trade is fulfilled. Marking %s as closed ...', - trade - ) - - for trade in filter(lambda t: t.is_open, trades): - # Check if there is already an open order for this trade - orders = exchange.get_open_orders(trade.pair) - orders = [o for o in orders if o['id'] == trade.open_order_id] - if orders: - msg = 'There is an open order for {}: Order(total={}, remaining={}, type={}, id={})' \ - .format( - trade, - round(orders[0]['amount'], 8), - round(orders[0]['remaining'], 8), - orders[0]['type'], - orders[0]['id']) - logger.info(msg) - else: - # Update state - trade.open_order_id = None - # Check if we can sell our current pair - handle_trade(trade) + # Update state + trade.open_order_id = None + # Check if we can sell our current pair + handle_trade(trade) + Trade.session.flush() + except (ConnectionError, json.JSONDecodeError) as error: + msg = 'Got {} in _process()'.format(error.__class__.__name__) + logger.exception(msg) def close_trade_if_fulfilled(trade: Trade) -> bool: @@ -247,35 +214,43 @@ def init(config: dict, db_url: Optional[str] = None) -> None: persistence.init(config, db_url) exchange.init(config) + # Set initial application state + initial_state = config.get('initial_state') + if initial_state: + update_state(State[initial_state.upper()]) + else: + update_state(State.STOPPED) + def app(config: dict) -> None: - + """ + Main function which handles the application state + :param config: config as dict + :return: None + """ logger.info('Starting freqtrade %s', __version__) init(config) - try: - telegram.send_msg('*Status:* `trader started`') - logger.info('Trader started') + old_state = get_state() + logger.info('Initial State: %s', old_state) + telegram.send_msg('*Status:* `{}`'.format(old_state.name.lower())) while True: - state = get_state() - if state == State.TERMINATE: - return - elif state == State.PAUSED: + new_state = get_state() + # Log state transition + if new_state != old_state: + telegram.send_msg('*Status:* `{}`'.format(new_state.name.lower())) + logging.info('Changing state to: %s', new_state.name) + + if new_state == State.STOPPED: time.sleep(1) - elif state == State.RUNNING: - try: - _process() - Trade.session.flush() - except (ConnectionError, json.JSONDecodeError, ValueError) as error: - msg = 'Got {} during _process()'.format(error.__class__.__name__) - logger.exception(msg) - finally: - time.sleep(25) - except (RuntimeError, json.JSONDecodeError): - telegram.send_msg( - '*Status:* Got RuntimeError: ```\n{}\n```'.format(traceback.format_exc()) - ) - logger.exception('RuntimeError. Stopping trader ...') + elif new_state == State.RUNNING: + _process() + # We need to sleep here because otherwise we would run into bittrex rate limit + time.sleep(25) + old_state = new_state + except RuntimeError: + telegram.send_msg('*Status:* Got RuntimeError: ```\n{}\n```'.format(traceback.format_exc())) + logger.exception('RuntimeError. Trader stopped!') finally: telegram.send_msg('*Status:* `Trader has stopped`') diff --git a/misc.py b/misc.py index 0564d0ba0..2d653651d 100644 --- a/misc.py +++ b/misc.py @@ -1,3 +1,36 @@ +import enum + +from wrapt import synchronized + + +class State(enum.Enum): + RUNNING = 0 + STOPPED = 1 + + +# Current application state +_STATE = State.STOPPED + + +@synchronized +def update_state(state: State) -> None: + """ + Updates the application state + :param state: new state + :return: None + """ + global _STATE + _STATE = state + + +@synchronized +def get_state() -> State: + """ + Gets the current application state + :return: + """ + return _STATE + # Required json-schema for user specified config CONF_SCHEMA = { @@ -25,7 +58,8 @@ CONF_SCHEMA = { 'chat_id': {'type': 'string'}, }, 'required': ['enabled', 'token', 'chat_id'] - } + }, + 'initial_state': {'type': 'string', 'enum': ['running', 'stopped']}, }, 'definitions': { 'exchange': { diff --git a/rpc/telegram.py b/rpc/telegram.py index cd046b749..80d557406 100644 --- a/rpc/telegram.py +++ b/rpc/telegram.py @@ -8,6 +8,7 @@ from telegram.error import NetworkError from telegram.ext import CommandHandler, Updater from telegram import ParseMode, Bot, Update +from misc import get_state, State, update_state from persistence import Trade import exchange @@ -89,7 +90,6 @@ def _status(bot: Bot, update: Update) -> None: """ # Fetch open trade trades = Trade.query.filter(Trade.is_open.is_(True)).all() - from main import get_state, State if get_state() != State.RUNNING: send_msg('*Status:* `trader is not running`', bot=bot) elif not trades: @@ -200,7 +200,6 @@ def _start(bot: Bot, update: Update) -> None: :param update: message update :return: None """ - from main import get_state, State, update_state if get_state() == State.RUNNING: send_msg('*Status:* `already running`', bot=bot) else: @@ -216,10 +215,9 @@ def _stop(bot: Bot, update: Update) -> None: :param update: message update :return: None """ - from main import get_state, State, update_state if get_state() == State.RUNNING: send_msg('`Stopping trader ...`', bot=bot) - update_state(State.PAUSED) + update_state(State.STOPPED) else: send_msg('*Status:* `already stopped`', bot=bot) @@ -233,7 +231,6 @@ def _forcesell(bot: Bot, update: Update) -> None: :param update: message update :return: None """ - from main import get_state, State if get_state() != State.RUNNING: send_msg('`trader is not running`', bot=bot) return @@ -281,7 +278,6 @@ def _performance(bot: Bot, update: Update) -> None: :param update: message update :return: None """ - from main import get_state, State if get_state() != State.RUNNING: send_msg('`trader is not running`', bot=bot) return diff --git a/test/test_telegram.py b/test/test_telegram.py index de6e3ed74..55b9c73f2 100644 --- a/test/test_telegram.py +++ b/test/test_telegram.py @@ -6,8 +6,8 @@ from jsonschema import validate from telegram import Bot, Update, Message, Chat import exchange -from main import init, create_trade, update_state, State, get_state -from misc import CONF_SCHEMA +from main import init, create_trade +from misc import CONF_SCHEMA, update_state, State, get_state from persistence import Trade from rpc.telegram import _status, _profit, _forcesell, _performance, _start, _stop @@ -46,7 +46,8 @@ class TestTelegram(unittest.TestCase): "enabled": True, "token": "token", "chat_id": "0" - } + }, + "initial_state": "running" } def test_1_status_handle(self): @@ -165,8 +166,8 @@ class TestTelegram(unittest.TestCase): with patch.multiple('main.telegram', _CONF=self.conf, init=MagicMock(), send_msg=msg_mock): init(self.conf, 'sqlite://') - update_state(State.PAUSED) - self.assertEqual(get_state(), State.PAUSED) + update_state(State.STOPPED) + self.assertEqual(get_state(), State.STOPPED) _start(bot=MagicBot(), update=self.update) self.assertEqual(get_state(), State.RUNNING) self.assertEqual(msg_mock.call_count, 0) @@ -180,7 +181,7 @@ class TestTelegram(unittest.TestCase): update_state(State.RUNNING) self.assertEqual(get_state(), State.RUNNING) _stop(bot=MagicBot(), update=self.update) - self.assertEqual(get_state(), State.PAUSED) + self.assertEqual(get_state(), State.STOPPED) self.assertEqual(msg_mock.call_count, 1) self.assertIn('Stopping trader', msg_mock.call_args_list[0][0][0])