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"
This commit is contained in:
gcarq 2017-09-09 00:31:40 +02:00
parent 996beae770
commit a4b2f4e4b9
6 changed files with 118 additions and 107 deletions

View File

@ -40,10 +40,14 @@ See the example below:
For example value `-0.10` will cause immediate sell if the For example value `-0.10` will cause immediate sell if the
profit dips below -10% for a given trade. This parameter is optional. 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, The other values should be self-explanatory,
if not feel free to raise a github issue. if not feel free to raise a github issue.
##### Prerequisites #### Prerequisites
* python3.6 * python3.6
* sqlite * sqlite
* [TA-lib](https://github.com/mrjbq7/ta-lib#dependencies) binaries * [TA-lib](https://github.com/mrjbq7/ta-lib#dependencies) binaries

View File

@ -35,5 +35,6 @@
"enabled": true, "enabled": true,
"token": "token", "token": "token",
"chat_id": "chat_id" "chat_id": "chat_id"
} },
"initial_state": "running"
} }

159
main.py
View File

@ -1,5 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python
import enum
import json import json
import logging import logging
import time import time
@ -8,13 +7,12 @@ from datetime import datetime
from typing import Optional from typing import Optional
from jsonschema import validate from jsonschema import validate
from wrapt import synchronized
import exchange import exchange
import persistence import persistence
from persistence import Trade from persistence import Trade
from analyze import get_buy_signal 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 from rpc import telegram
logging.basicConfig(level=logging.DEBUG, logging.basicConfig(level=logging.DEBUG,
@ -26,38 +24,8 @@ __copyright__ = "gcarq 2017"
__license__ = "GPLv3" __license__ = "GPLv3"
__version__ = "0.8.0" __version__ = "0.8.0"
class State(enum.Enum):
RUNNING = 0
PAUSED = 1
TERMINATE = 2
_CONF = {} _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: def _process() -> None:
""" """
@ -65,44 +33,43 @@ def _process() -> None:
otherwise a new trade is created. otherwise a new trade is created.
:return: None :return: None
""" """
# Query trades from persistence layer try:
trades = Trade.query.filter(Trade.is_open.is_(True)).all() # Query trades from persistence layer
if len(trades) < _CONF['max_open_trades']: trades = Trade.query.filter(Trade.is_open.is_(True)).all()
try: if len(trades) < _CONF['max_open_trades']:
# Create entity and execute trade try:
trade = create_trade(float(_CONF['stake_amount']), exchange.EXCHANGE) # Create entity and execute trade
if trade: trade = create_trade(float(_CONF['stake_amount']), exchange.EXCHANGE)
Trade.session.add(trade) 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: else:
logging.info('Got no buy signal...') # Update state
except ValueError: trade.open_order_id = None
logger.exception('Unable to create trade') # Check if we can sell our current pair
handle_trade(trade)
for trade in trades: Trade.session.flush()
if close_trade_if_fulfilled(trade): except (ConnectionError, json.JSONDecodeError) as error:
logger.info( msg = 'Got {} in _process()'.format(error.__class__.__name__)
'No open orders found and trade is fulfilled. Marking %s as closed ...', logger.exception(msg)
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)
def close_trade_if_fulfilled(trade: Trade) -> bool: 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) persistence.init(config, db_url)
exchange.init(config) 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: 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__) logger.info('Starting freqtrade %s', __version__)
init(config) init(config)
try: try:
telegram.send_msg('*Status:* `trader started`') old_state = get_state()
logger.info('Trader started') logger.info('Initial State: %s', old_state)
telegram.send_msg('*Status:* `{}`'.format(old_state.name.lower()))
while True: while True:
state = get_state() new_state = get_state()
if state == State.TERMINATE: # Log state transition
return if new_state != old_state:
elif state == State.PAUSED: 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) time.sleep(1)
elif state == State.RUNNING: elif new_state == State.RUNNING:
try: _process()
_process() # We need to sleep here because otherwise we would run into bittrex rate limit
Trade.session.flush() time.sleep(25)
except (ConnectionError, json.JSONDecodeError, ValueError) as error: old_state = new_state
msg = 'Got {} during _process()'.format(error.__class__.__name__) except RuntimeError:
logger.exception(msg) telegram.send_msg('*Status:* Got RuntimeError: ```\n{}\n```'.format(traceback.format_exc()))
finally: logger.exception('RuntimeError. Trader stopped!')
time.sleep(25)
except (RuntimeError, json.JSONDecodeError):
telegram.send_msg(
'*Status:* Got RuntimeError: ```\n{}\n```'.format(traceback.format_exc())
)
logger.exception('RuntimeError. Stopping trader ...')
finally: finally:
telegram.send_msg('*Status:* `Trader has stopped`') telegram.send_msg('*Status:* `Trader has stopped`')

36
misc.py
View File

@ -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 # Required json-schema for user specified config
CONF_SCHEMA = { CONF_SCHEMA = {
@ -25,7 +58,8 @@ CONF_SCHEMA = {
'chat_id': {'type': 'string'}, 'chat_id': {'type': 'string'},
}, },
'required': ['enabled', 'token', 'chat_id'] 'required': ['enabled', 'token', 'chat_id']
} },
'initial_state': {'type': 'string', 'enum': ['running', 'stopped']},
}, },
'definitions': { 'definitions': {
'exchange': { 'exchange': {

View File

@ -8,6 +8,7 @@ from telegram.error import NetworkError
from telegram.ext import CommandHandler, Updater from telegram.ext import CommandHandler, Updater
from telegram import ParseMode, Bot, Update from telegram import ParseMode, Bot, Update
from misc import get_state, State, update_state
from persistence import Trade from persistence import Trade
import exchange import exchange
@ -89,7 +90,6 @@ def _status(bot: Bot, update: Update) -> None:
""" """
# Fetch open trade # Fetch open trade
trades = Trade.query.filter(Trade.is_open.is_(True)).all() trades = Trade.query.filter(Trade.is_open.is_(True)).all()
from main import get_state, State
if get_state() != State.RUNNING: if get_state() != State.RUNNING:
send_msg('*Status:* `trader is not running`', bot=bot) send_msg('*Status:* `trader is not running`', bot=bot)
elif not trades: elif not trades:
@ -200,7 +200,6 @@ def _start(bot: Bot, update: Update) -> None:
:param update: message update :param update: message update
:return: None :return: None
""" """
from main import get_state, State, update_state
if get_state() == State.RUNNING: if get_state() == State.RUNNING:
send_msg('*Status:* `already running`', bot=bot) send_msg('*Status:* `already running`', bot=bot)
else: else:
@ -216,10 +215,9 @@ def _stop(bot: Bot, update: Update) -> None:
:param update: message update :param update: message update
:return: None :return: None
""" """
from main import get_state, State, update_state
if get_state() == State.RUNNING: if get_state() == State.RUNNING:
send_msg('`Stopping trader ...`', bot=bot) send_msg('`Stopping trader ...`', bot=bot)
update_state(State.PAUSED) update_state(State.STOPPED)
else: else:
send_msg('*Status:* `already stopped`', bot=bot) send_msg('*Status:* `already stopped`', bot=bot)
@ -233,7 +231,6 @@ def _forcesell(bot: Bot, update: Update) -> None:
:param update: message update :param update: message update
:return: None :return: None
""" """
from main import get_state, State
if get_state() != State.RUNNING: if get_state() != State.RUNNING:
send_msg('`trader is not running`', bot=bot) send_msg('`trader is not running`', bot=bot)
return return
@ -281,7 +278,6 @@ def _performance(bot: Bot, update: Update) -> None:
:param update: message update :param update: message update
:return: None :return: None
""" """
from main import get_state, State
if get_state() != State.RUNNING: if get_state() != State.RUNNING:
send_msg('`trader is not running`', bot=bot) send_msg('`trader is not running`', bot=bot)
return return

View File

@ -6,8 +6,8 @@ from jsonschema import validate
from telegram import Bot, Update, Message, Chat from telegram import Bot, Update, Message, Chat
import exchange import exchange
from main import init, create_trade, update_state, State, get_state from main import init, create_trade
from misc import CONF_SCHEMA from misc import CONF_SCHEMA, update_state, State, get_state
from persistence import Trade from persistence import Trade
from rpc.telegram import _status, _profit, _forcesell, _performance, _start, _stop from rpc.telegram import _status, _profit, _forcesell, _performance, _start, _stop
@ -46,7 +46,8 @@ class TestTelegram(unittest.TestCase):
"enabled": True, "enabled": True,
"token": "token", "token": "token",
"chat_id": "0" "chat_id": "0"
} },
"initial_state": "running"
} }
def test_1_status_handle(self): 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): with patch.multiple('main.telegram', _CONF=self.conf, init=MagicMock(), send_msg=msg_mock):
init(self.conf, 'sqlite://') init(self.conf, 'sqlite://')
update_state(State.PAUSED) update_state(State.STOPPED)
self.assertEqual(get_state(), State.PAUSED) self.assertEqual(get_state(), State.STOPPED)
_start(bot=MagicBot(), update=self.update) _start(bot=MagicBot(), update=self.update)
self.assertEqual(get_state(), State.RUNNING) self.assertEqual(get_state(), State.RUNNING)
self.assertEqual(msg_mock.call_count, 0) self.assertEqual(msg_mock.call_count, 0)
@ -180,7 +181,7 @@ class TestTelegram(unittest.TestCase):
update_state(State.RUNNING) update_state(State.RUNNING)
self.assertEqual(get_state(), State.RUNNING) self.assertEqual(get_state(), State.RUNNING)
_stop(bot=MagicBot(), update=self.update) _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.assertEqual(msg_mock.call_count, 1)
self.assertIn('Stopping trader', msg_mock.call_args_list[0][0][0]) self.assertIn('Stopping trader', msg_mock.call_args_list[0][0][0])