From 0e1eb20781a71e6e9468f44f96f82545c678d907 Mon Sep 17 00:00:00 2001 From: Sebastien Moreau Date: Mon, 23 Oct 2017 16:43:05 -0400 Subject: [PATCH 1/5] Adds /count command Adds /count command Adds /count command --- .editorconfig | 18 ++++++++++ bin/run-docker | 74 +++++++++++++++++++++++++++++++++++++++ freqtrade/rpc/telegram.py | 24 +++++++++++++ 3 files changed, 116 insertions(+) create mode 100644 .editorconfig create mode 100755 bin/run-docker diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..326ade84f --- /dev/null +++ b/.editorconfig @@ -0,0 +1,18 @@ +# http://editorconfig.org +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +max_line_length = 80 +trim_trailing_whitespace = true + +[*.md] +max_line_length = 0 +trim_trailing_whitespace = false + +[COMMIT_EDITMSG] +max_line_length = 0 diff --git a/bin/run-docker b/bin/run-docker new file mode 100755 index 000000000..e0874589b --- /dev/null +++ b/bin/run-docker @@ -0,0 +1,74 @@ +#!/bin/bash + +# Check if docker image exists +CMD_CHECK_IMAGE="docker images -q freqtrade:latest" +REBUILD=false +DRY_RUN=false + +while getopts rdh option +do + case "${option}" + in + r) REBUILD=true + ;; + d) DRY_RUN=true + ;; + h) cat << EOF + +Commands available : + +-r Rebuild the container +-d Dry Run +-h This help message +EOF + exit 0 + ;; + esac +done + +# Ensures files exists +[ -d ~/.freqtrade ] || mkdir ~/.freqtrade +cp config.json ~/.freqtrade/ +touch ~/.freqtrade/tradesv2.sqlite +touch ~/.freqtrade/tradesv2.dry_run.sqlite + + +echo 'Stopping container...' +docker stop freqtrade > /dev/null \ + && docker rm freqtrade > /dev/null \ + && echo 'Container stopped' + + +if [[ -z $($CMD_CHECK_IMAGE) || $REBUILD = true ]]; then + echo "Building container" + docker build -t freqtrade . +fi + + +# Generates Docker commands based on options +DOCKER_CMD="docker run -d \ + --name freqtrade \ + -v ~/.freqtrade/config.json:/freqtrade/config.json" + +if [[ $DRY_RUN = true ]]; then + DOCKER_CMD="$DOCKER_CMD \ + -v ~/.freqtrade/tradesv2.dry_run.sqlite:/freqtrade/tradesv2.dry_run.sqlite" +else + DOCKER_CMD="$DOCKER_CMD \ + -v ~/.freqtrade/tradesv2.sqlite:/freqtrade/tradesv2.sqlite" +fi + +DOCKER_CMD="$DOCKER_CMD freqtrade" + +echo 'Starting container' +eval $DOCKER_CMD \ > /dev/null \ + && echo 'Container ready' \ + || echo 'Problem starting container' + +exit 0 + +docker run -d \ + --name freqtrade \ + -v ~/.freqtrade/config.json:/freqtrade/config.json \ + -v ~/.freqtrade/tradesv2.dry_run.sqlite:/freqtrade/tradesv2.dry_run.sqlite \ + freqtrade diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 84c86f816..ecac558c9 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -45,6 +45,7 @@ def init(config: dict) -> None: CommandHandler('stop', _stop), CommandHandler('forcesell', _forcesell), CommandHandler('performance', _performance), + CommandHandler('count', _count), CommandHandler('help', _help), ] for handle in handles: @@ -310,6 +311,29 @@ def _performance(bot: Bot, update: Update) -> None: send_msg(message, parse_mode=ParseMode.HTML) +@authorized_only +def _count(bot: Bot, update: Update) -> None: + """ + Handler for /count. + Returns the number of trades running + :param bot: telegram bot + :param update: message update + :return: None + """ + if get_state() != State.RUNNING: + send_msg('`trader is not running`', bot=bot) + return + + trades = Trade.query.filter(Trade.is_open.is_(True)).all() + + current_trades_count = len(trades) + max_trades_count = _CONF['max_open_trades'] + + message = 'Count:\ncurrent/max\n{}/{}\n'.format(current_trades_count, max_trades_count) + logger.debug(message) + send_msg(message, parse_mode=ParseMode.HTML) + + @authorized_only def _help(bot: Bot, update: Update) -> None: """ From 8bdace68f60ded15f30bf34afd7f41ad8244535b Mon Sep 17 00:00:00 2001 From: Sebastien Moreau Date: Sun, 29 Oct 2017 18:57:48 -0400 Subject: [PATCH 2/5] Adds options for /status command --- .editorconfig | 18 -------- bin/run-docker | 74 ------------------------------ freqtrade/rpc/telegram.py | 94 ++++++++++++++++++++++++++++++++++++++- requirements.txt | 3 +- 4 files changed, 95 insertions(+), 94 deletions(-) delete mode 100644 .editorconfig delete mode 100755 bin/run-docker diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index 326ade84f..000000000 --- a/.editorconfig +++ /dev/null @@ -1,18 +0,0 @@ -# http://editorconfig.org -root = true - -[*] -charset = utf-8 -end_of_line = lf -indent_size = 4 -indent_style = space -insert_final_newline = true -max_line_length = 80 -trim_trailing_whitespace = true - -[*.md] -max_line_length = 0 -trim_trailing_whitespace = false - -[COMMIT_EDITMSG] -max_line_length = 0 diff --git a/bin/run-docker b/bin/run-docker deleted file mode 100755 index e0874589b..000000000 --- a/bin/run-docker +++ /dev/null @@ -1,74 +0,0 @@ -#!/bin/bash - -# Check if docker image exists -CMD_CHECK_IMAGE="docker images -q freqtrade:latest" -REBUILD=false -DRY_RUN=false - -while getopts rdh option -do - case "${option}" - in - r) REBUILD=true - ;; - d) DRY_RUN=true - ;; - h) cat << EOF - -Commands available : - --r Rebuild the container --d Dry Run --h This help message -EOF - exit 0 - ;; - esac -done - -# Ensures files exists -[ -d ~/.freqtrade ] || mkdir ~/.freqtrade -cp config.json ~/.freqtrade/ -touch ~/.freqtrade/tradesv2.sqlite -touch ~/.freqtrade/tradesv2.dry_run.sqlite - - -echo 'Stopping container...' -docker stop freqtrade > /dev/null \ - && docker rm freqtrade > /dev/null \ - && echo 'Container stopped' - - -if [[ -z $($CMD_CHECK_IMAGE) || $REBUILD = true ]]; then - echo "Building container" - docker build -t freqtrade . -fi - - -# Generates Docker commands based on options -DOCKER_CMD="docker run -d \ - --name freqtrade \ - -v ~/.freqtrade/config.json:/freqtrade/config.json" - -if [[ $DRY_RUN = true ]]; then - DOCKER_CMD="$DOCKER_CMD \ - -v ~/.freqtrade/tradesv2.dry_run.sqlite:/freqtrade/tradesv2.dry_run.sqlite" -else - DOCKER_CMD="$DOCKER_CMD \ - -v ~/.freqtrade/tradesv2.sqlite:/freqtrade/tradesv2.sqlite" -fi - -DOCKER_CMD="$DOCKER_CMD freqtrade" - -echo 'Starting container' -eval $DOCKER_CMD \ > /dev/null \ - && echo 'Container ready' \ - || echo 'Problem starting container' - -exit 0 - -docker run -d \ - --name freqtrade \ - -v ~/.freqtrade/config.json:/freqtrade/config.json \ - -v ~/.freqtrade/tradesv2.dry_run.sqlite:/freqtrade/tradesv2.dry_run.sqlite \ - freqtrade diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index ecac558c9..94449fa70 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -1,6 +1,8 @@ import logging from datetime import timedelta from typing import Callable, Any +from pandas import DataFrame +from tabulate import tabulate import arrow from sqlalchemy import and_, func, text @@ -100,6 +102,14 @@ def _status(bot: Bot, update: Update) -> None: :param update: message update :return: None """ + + # Check if additional parameters are passed + params = update.message.text.replace('/status', '').split(' ') \ + if update.message.text else [] + if 'table' in params: + _status_table(bot, update) + return + # Fetch open trade trades = Trade.query.filter(Trade.is_open.is_(True)).all() if get_state() != State.RUNNING: @@ -145,6 +155,71 @@ def _status(bot: Bot, update: Update) -> None: send_msg(message, bot=bot) +@authorized_only +def _status_table(bot: Bot, update: Update) -> None: + """ + Handler for /status table. + Returns the current TradeThread status in table format + :param bot: telegram bot + :param update: message update + :return: None + """ + short_version = False + params = update.message.text.replace('/status', '').split(' ') + if 'short' in params: + short_version = True + + # Fetch open trade + trades = Trade.query.filter(Trade.is_open.is_(True)).all() + if get_state() != State.RUNNING: + send_msg('*Status:* `trader is not running`', bot=bot) + elif not trades: + send_msg('*Status:* `no active order`', bot=bot) + else: + trades_list = [] + for trade in trades: + # calculate profit and send message to user + current_rate = exchange.get_ticker(trade.pair)['bid'] + current_profit = 100 * ((current_rate - trade.open_rate) / trade.open_rate) + orders = exchange.get_open_orders(trade.pair) + orders = [o for o in orders if o['id'] == trade.open_order_id] + order = orders[0] if orders else None + + fmt_close_profit = '{:.2f}'.format(trade.close_profit) if trade.close_profit else 'No' + fmt_current_profit = '{:.2f}'.format(current_profit) + + row = [ + trade.id, + trade.pair, + shorten_date(arrow.get(trade.open_date).humanize()), + round(trade.amount, 8), + trade.open_rate, + trade.close_rate, + current_rate, + fmt_close_profit, + fmt_current_profit, + '{} ({})'.format(order['remaining'], order['type']) if order else 'No' + ] + + trades_list.append(row) + + columns = ['ID', 'Pair', 'Since', 'Amount', 'Open Rate', + 'Close Rate', 'Cur. Rate', 'Close Profit', 'Profit', + 'Open Order'] + + df = DataFrame.from_records(trades_list, columns=columns) + df = df.set_index(columns[0]) + + columns_short = ['Pair', 'Since', 'Profit'] + + if short_version == True: + df = df[columns_short] + + message = tabulate(df, headers='keys', tablefmt='simple') + message = "
{}
".format(message) + + send_msg(message, parse_mode=ParseMode.HTML) + @authorized_only def _profit(bot: Bot, update: Update) -> None: """ @@ -346,15 +421,32 @@ def _help(bot: Bot, update: Update) -> None: message = """ */start:* `Starts the trader` */stop:* `Stops the trader` -*/status:* `Lists all open trades` +*/status [table [short]]:* `Lists all open trades` + *table :* `will display trades in a table` + *short :* `condensed output` */profit:* `Lists cumulative profit from all finished trades` */forcesell :* `Instantly sells the given trade, regardless of profit` */performance:* `Show performance of each finished trade grouped by pair` +*/count:* `Show number of trades running compared to allowed number of trades` */help:* `This help message` """ send_msg(message, bot=bot) +def shorten_date(date): + return date.replace('ago', '') \ + .replace('seconds', 's') \ + .replace('minutes', 'm') \ + .replace('minute', 'm') \ + .replace('hours', 'h') \ + .replace('hour', 'h') \ + .replace('days', 'd') \ + .replace('day', 'd') \ + .replace('an', '1') \ + .replace('a', '1') \ + .replace(' ', '') + + def send_msg(msg: str, bot: Bot = None, parse_mode: ParseMode = ParseMode.MARKDOWN) -> None: """ Send given markdown message diff --git a/requirements.txt b/requirements.txt index 69379b025..83292154e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,7 +17,8 @@ pytest-cov==2.5.1 hyperopt==0.1 # do not upgrade networkx before this is fixed https://github.com/hyperopt/hyperopt/issues/325 networkx==1.11 +tabulate==0.8.1 # Required for plotting data #matplotlib==2.1.0 -#PYQT5==5.9 \ No newline at end of file +#PYQT5==5.9 From 361bdd20d35e2823e0cc10e63f807f342f8df614 Mon Sep 17 00:00:00 2001 From: Sebastien Moreau Date: Sun, 29 Oct 2017 20:55:14 -0400 Subject: [PATCH 3/5] Updates README --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4388f00e3..edc21a9b5 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,8 @@ Persistence is achieved through sqlite. #### Telegram RPC commands: * /start: Starts the trader * /stop: Stops the trader -* /status: Lists all open trades +* /status [table [short]]: Lists all open trades +* /count: Displays number of open trades * /profit: Lists cumulative profit from all finished trades * /forcesell : Instantly sells the given trade (Ignoring `minimum_roi`). * /performance: Show performance of each finished trade grouped by pair From 60249af04ccec3f31c1bdf541ec1a70b85a9f8e6 Mon Sep 17 00:00:00 2001 From: Sebastien Moreau Date: Thu, 2 Nov 2017 13:25:19 -0400 Subject: [PATCH 4/5] Removes long format + pylint fixes --- README.md | 2 +- freqtrade/rpc/telegram.py | 71 ++++++++++++--------------------------- 2 files changed, 23 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index edc21a9b5..aa6d1a506 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Persistence is achieved through sqlite. #### Telegram RPC commands: * /start: Starts the trader * /stop: Stops the trader -* /status [table [short]]: Lists all open trades +* /status [table]: Lists all open trades * /count: Displays number of open trades * /profit: Lists cumulative profit from all finished trades * /forcesell : Instantly sells the given trade (Ignoring `minimum_roi`). diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 94449fa70..dc13726f7 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -1,4 +1,5 @@ import logging +import re from datetime import timedelta from typing import Callable, Any from pandas import DataFrame @@ -164,11 +165,6 @@ def _status_table(bot: Bot, update: Update) -> None: :param update: message update :return: None """ - short_version = False - params = update.message.text.replace('/status', '').split(' ') - if 'short' in params: - short_version = True - # Fetch open trade trades = Trade.query.filter(Trade.is_open.is_(True)).all() if get_state() != State.RUNNING: @@ -180,46 +176,29 @@ def _status_table(bot: Bot, update: Update) -> None: for trade in trades: # calculate profit and send message to user current_rate = exchange.get_ticker(trade.pair)['bid'] - current_profit = 100 * ((current_rate - trade.open_rate) / trade.open_rate) - orders = exchange.get_open_orders(trade.pair) - orders = [o for o in orders if o['id'] == trade.open_order_id] - order = orders[0] if orders else None - - fmt_close_profit = '{:.2f}'.format(trade.close_profit) if trade.close_profit else 'No' - fmt_current_profit = '{:.2f}'.format(current_profit) + current_profit = '{:.2f}'.format(100 * ((current_rate \ + - trade.open_rate) / trade.open_rate)) row = [ trade.id, trade.pair, - shorten_date(arrow.get(trade.open_date).humanize()), - round(trade.amount, 8), - trade.open_rate, - trade.close_rate, - current_rate, - fmt_close_profit, - fmt_current_profit, - '{} ({})'.format(order['remaining'], order['type']) if order else 'No' + shorten_date(arrow.get(trade.open_date).humanize(only_distance=True)), + current_profit ] trades_list.append(row) - columns = ['ID', 'Pair', 'Since', 'Amount', 'Open Rate', - 'Close Rate', 'Cur. Rate', 'Close Profit', 'Profit', - 'Open Order'] + columns = ['ID', 'Pair', 'Since', 'Profit'] - df = DataFrame.from_records(trades_list, columns=columns) - df = df.set_index(columns[0]) + df_statuses = DataFrame.from_records(trades_list, columns=columns) + df_statuses = df_statuses.set_index(columns[0]) - columns_short = ['Pair', 'Since', 'Profit'] - - if short_version == True: - df = df[columns_short] - - message = tabulate(df, headers='keys', tablefmt='simple') + message = tabulate(df_statuses, headers='keys', tablefmt='simple') message = "
{}
".format(message) send_msg(message, parse_mode=ParseMode.HTML) + @authorized_only def _profit(bot: Bot, update: Update) -> None: """ @@ -387,7 +366,7 @@ def _performance(bot: Bot, update: Update) -> None: @authorized_only -def _count(bot: Bot, update: Update) -> None: +def _count(bot: Bot) -> None: """ Handler for /count. Returns the number of trades running @@ -400,11 +379,8 @@ def _count(bot: Bot, update: Update) -> None: return trades = Trade.query.filter(Trade.is_open.is_(True)).all() + message = 'Count:\ncurrent/max\n{}/{}\n'.format(len(trades), _CONF['max_open_trades']) - current_trades_count = len(trades) - max_trades_count = _CONF['max_open_trades'] - - message = 'Count:\ncurrent/max\n{}/{}\n'.format(current_trades_count, max_trades_count) logger.debug(message) send_msg(message, parse_mode=ParseMode.HTML) @@ -421,9 +397,8 @@ def _help(bot: Bot, update: Update) -> None: message = """ */start:* `Starts the trader` */stop:* `Stops the trader` -*/status [table [short]]:* `Lists all open trades` +*/status [table]:* `Lists all open trades` *table :* `will display trades in a table` - *short :* `condensed output` */profit:* `Lists cumulative profit from all finished trades` */forcesell :* `Instantly sells the given trade, regardless of profit` */performance:* `Show performance of each finished trade grouped by pair` @@ -434,17 +409,15 @@ def _help(bot: Bot, update: Update) -> None: def shorten_date(date): - return date.replace('ago', '') \ - .replace('seconds', 's') \ - .replace('minutes', 'm') \ - .replace('minute', 'm') \ - .replace('hours', 'h') \ - .replace('hour', 'h') \ - .replace('days', 'd') \ - .replace('day', 'd') \ - .replace('an', '1') \ - .replace('a', '1') \ - .replace(' ', '') + """ + Trim the date so it fits on small screens + """ + new_date = re.sub('seconds?', 'sec', date) + new_date = re.sub('minutes?', 'min', new_date) + new_date = re.sub('hours?', 'h', new_date) + new_date = re.sub('days?', 'd', new_date) + new_date = re.sub('^an?', '1', new_date) + return new_date def send_msg(msg: str, bot: Bot = None, parse_mode: ParseMode = ParseMode.MARKDOWN) -> None: From caa6e22e5312d401f4d3f9eaf540fa18a8b55daf Mon Sep 17 00:00:00 2001 From: Sebastien Moreau Date: Sun, 5 Nov 2017 10:26:03 -0500 Subject: [PATCH 5/5] Adds unit tests --- freqtrade/rpc/telegram.py | 2 +- freqtrade/tests/test_telegram.py | 75 +++++++++++++++++++++++++++++++- 2 files changed, 74 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index dc13726f7..4497f68eb 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -366,7 +366,7 @@ def _performance(bot: Bot, update: Update) -> None: @authorized_only -def _count(bot: Bot) -> None: +def _count(bot: Bot, update: Update) -> None: """ Handler for /count. Returns the number of trades running diff --git a/freqtrade/tests/test_telegram.py b/freqtrade/tests/test_telegram.py index fb9a618a0..74924d5c8 100644 --- a/freqtrade/tests/test_telegram.py +++ b/freqtrade/tests/test_telegram.py @@ -1,4 +1,6 @@ # pragma pylint: disable=missing-docstring +import logging +import re from datetime import datetime from unittest.mock import MagicMock @@ -9,7 +11,12 @@ from telegram import Bot, Update, Message, Chat from freqtrade.main import init, create_trade from freqtrade.misc import update_state, State, get_state, CONF_SCHEMA from freqtrade.persistence import Trade -from freqtrade.rpc.telegram import _status, _profit, _forcesell, _performance, _start, _stop +from freqtrade.rpc.telegram import _status, _status_table, _profit, _forcesell, _performance, \ + _count, _start, _stop + +logging.getLogger('requests.packages.urllib3').setLevel(logging.INFO) +logging.getLogger('telegram').setLevel(logging.INFO) +logger = logging.getLogger(__name__) @pytest.fixture @@ -33,7 +40,8 @@ def conf(): "key": "key", "secret": "secret", "pair_whitelist": [ - "BTC_ETH" + "BTC_ETH", + "BTC_ETC" ] }, "telegram": { @@ -82,6 +90,39 @@ def test_status_handle(conf, update, mocker): assert msg_mock.call_count == 2 assert '[BTC_ETH]' in msg_mock.call_args_list[-1][0][0] + +def test_status_table_handle(conf, update, mocker): + mocker.patch.dict('freqtrade.main._CONF', conf) + mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True) + msg_mock = MagicMock() + mocker.patch.multiple('freqtrade.main.telegram', _CONF=conf, init=MagicMock(), send_msg=msg_mock) + mocker.patch.multiple('freqtrade.main.exchange', + validate_pairs=MagicMock(), + get_ticker=MagicMock(return_value={ + 'bid': 0.07256061, + 'ask': 0.072661, + 'last': 0.07256061 + }), + buy=MagicMock(return_value='mocked_order_id')) + init(conf, 'sqlite://') + + # Create some test data + trade = create_trade(15.0) + assert trade + Trade.session.add(trade) + Trade.session.flush() + + _status_table(bot=MagicBot(), update=update) + + text = re.sub('<\/?pre>', '', msg_mock.call_args_list[-1][0][0]) + line = text.split("\n") + fields = re.sub('[ ]+', ' ', line[2].strip()).split(' ') + + assert int(fields[0]) == 1 + assert fields[1] == 'BTC_ETH' + assert msg_mock.call_count == 2 + + def test_profit_handle(conf, update, mocker): mocker.patch.dict('freqtrade.main._CONF', conf) mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True) @@ -171,6 +212,36 @@ def test_performance_handle(conf, update, mocker): assert 'Performance' in msg_mock.call_args_list[-1][0][0] assert 'BTC_ETH 100.00%' in msg_mock.call_args_list[-1][0][0] + +def test_count_handle(conf, update, mocker): + mocker.patch.dict('freqtrade.main._CONF', conf) + mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True) + msg_mock = MagicMock() + mocker.patch.multiple('freqtrade.main.telegram', _CONF=conf, init=MagicMock(), send_msg=msg_mock) + mocker.patch.multiple('freqtrade.main.exchange', + validate_pairs=MagicMock(), + get_ticker=MagicMock(return_value={ + 'bid': 0.07256061, + 'ask': 0.072661, + 'last': 0.07256061 + }), + buy=MagicMock(return_value='mocked_order_id')) + init(conf, 'sqlite://') + + # Create some test data + trade = create_trade(15.0) + trade2 = create_trade(15.0) + assert trade + assert trade2 + Trade.session.add(trade) + Trade.session.add(trade2) + Trade.session.flush() + + _count(bot=MagicBot(), update=update) + line = msg_mock.call_args_list[-1][0][0].split("\n") + assert line[2] == '{}/{}'.format(2, conf['max_open_trades']) + + def test_start_handle(conf, update, mocker): mocker.patch.dict('freqtrade.main._CONF', conf) msg_mock = MagicMock()