From 0cfbb56b6ccf6b092d532f810f0e21a421faa2dd Mon Sep 17 00:00:00 2001 From: gcarq Date: Thu, 9 Nov 2017 21:47:47 +0100 Subject: [PATCH 01/10] enhance and test pair validation --- freqtrade/exchange/__init__.py | 5 +++++ freqtrade/tests/test_exchange.py | 34 ++++++++++++++++++++++++++++++++ freqtrade/tests/test_telegram.py | 3 ++- 3 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 freqtrade/tests/test_exchange.py diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 87bd0bbd1..feebd6bf1 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -63,7 +63,12 @@ def validate_pairs(pairs: List[str]) -> None: :return: None """ markets = _API.get_markets() + stake_cur = _CONF['stake_currency'] for pair in pairs: + if not pair.startswith(stake_cur): + raise RuntimeError( + '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())) diff --git a/freqtrade/tests/test_exchange.py b/freqtrade/tests/test_exchange.py new file mode 100644 index 000000000..d4d4e2588 --- /dev/null +++ b/freqtrade/tests/test_exchange.py @@ -0,0 +1,34 @@ +# pragma pylint: disable=missing-docstring +from unittest.mock import MagicMock + +import pytest + +from freqtrade.exchange import validate_pairs + + +def test_validate_pairs(default_conf, mocker): + api_mock = MagicMock() + api_mock.get_markets = MagicMock(return_value=['BTC_ETH', 'BTC_TKN', 'BTC_TRST', 'BTC_SWT']) + mocker.patch('freqtrade.exchange._API', api_mock) + mocker.patch.dict('freqtrade.exchange._CONF', default_conf) + validate_pairs(default_conf['exchange']['pair_whitelist']) + + +def test_validate_pairs_not_available(default_conf, mocker): + api_mock = MagicMock() + 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'): + validate_pairs(default_conf['exchange']['pair_whitelist']) + + +def test_validate_pairs_not_compatible(default_conf, mocker): + api_mock = MagicMock() + api_mock.get_markets = MagicMock(return_value=['BTC_ETH', 'BTC_TKN', 'BTC_TRST', 'BTC_SWT']) + 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'): + validate_pairs(default_conf['exchange']['pair_whitelist']) + diff --git a/freqtrade/tests/test_telegram.py b/freqtrade/tests/test_telegram.py index 0fc79ecad..7ca2619a7 100644 --- a/freqtrade/tests/test_telegram.py +++ b/freqtrade/tests/test_telegram.py @@ -268,7 +268,8 @@ def test_forcesell_handle_invalid(default_conf, update, mocker): assert 'no open trade' in msg_mock.call_args_list[0][0][0] -def test_performance_handle(default_conf, update, ticker, limit_buy_order, limit_sell_order, mocker): +def test_performance_handle( + default_conf, update, ticker, limit_buy_order, limit_sell_order, mocker): mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True) msg_mock = MagicMock() From 69ae99406ab8cac54e195b3f17b7e0214bb091a1 Mon Sep 17 00:00:00 2001 From: Eoin Date: Wed, 27 Sep 2017 22:32:04 +0100 Subject: [PATCH 02/10] add telegram handler for forcesellall --- freqtrade/rpc/telegram.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index bbdece23b..fa91e0f32 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -48,6 +48,7 @@ def init(config: dict) -> None: CommandHandler('start', _start), CommandHandler('stop', _stop), CommandHandler('forcesell', _forcesell), + CommandHandler('forcesellall', _forcesellall), CommandHandler('performance', _performance), CommandHandler('count', _count), CommandHandler('help', _help), @@ -402,6 +403,39 @@ def _count(bot: Bot, update: Update) -> None: logger.debug(message) send_msg(message, parse_mode=ParseMode.HTML) +@authorized_only +def _forcesellall(bot: Bot, update: Update) -> None: + """ + Handler for /forcesellall. + Sells all currently active trades + :param bot: telegram bot + :param update: message update + :return: None + """ + # 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: + for trade in trades: + # Get current rate + current_rate = exchange.get_ticker(trade.pair)['bid'] + # Get available balance + currency = trade.pair.split('_')[1] + balance = exchange.get_balance(currency) + # Execute sell + profit = trade.exec_sell_order(current_rate, balance) + message = '*{}:* Selling [{}]({}) at rate `{:f} (profit: {}%)`'.format( + trade.exchange.name, + trade.pair.replace('_', '/'), + exchange.get_pair_detail_url(trade.pair), + trade.close_rate, + round(profit, 2) + ) + logger.info(message) + send_msg(message) @authorized_only def _help(bot: Bot, update: Update) -> None: From ee05561ef37049e0d4eca75eff4ed9dcc4eb7a4c Mon Sep 17 00:00:00 2001 From: gcarq Date: Thu, 9 Nov 2017 22:07:51 +0100 Subject: [PATCH 03/10] refactor forcesellall to /forcesell all --- freqtrade/rpc/telegram.py | 73 +++++++++++---------------------------- 1 file changed, 21 insertions(+), 52 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index fa91e0f32..14478c2e3 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -48,7 +48,6 @@ def init(config: dict) -> None: CommandHandler('start', _start), CommandHandler('stop', _stop), CommandHandler('forcesell', _forcesell), - CommandHandler('forcesellall', _forcesellall), CommandHandler('performance', _performance), CommandHandler('count', _count), CommandHandler('help', _help), @@ -332,26 +331,29 @@ def _forcesell(bot: Bot, update: Update) -> None: send_msg('`trader is not running`', bot=bot) return - try: - trade_id = int(update.message.text - .replace('/forcesell', '') - .strip()) - # Query for trade - trade = Trade.query.filter(and_( - Trade.id == trade_id, - Trade.is_open.is_(True) - )).first() - if not trade: - send_msg('There is no open trade with ID: `{}`'.format(trade_id)) - return - # Get current rate - current_rate = exchange.get_ticker(trade.pair)['bid'] - from freqtrade.main import execute_sell - execute_sell(trade, current_rate) + trade_id = update.message.text.replace('/forcesell', '').strip() + if trade_id == 'all': + # Execute sell for all open orders + for trade in Trade.query.filter(Trade.is_open.is_(True)).all(): + # Get current rate + current_rate = exchange.get_ticker(trade.pair)['bid'] + from freqtrade.main import execute_sell + execute_sell(trade, current_rate) + return - except ValueError: + # Query for trade + trade = Trade.query.filter(and_( + Trade.id == trade_id, + Trade.is_open.is_(True) + )).first() + if not trade: send_msg('Invalid argument. Usage: `/forcesell `') logger.warning('/forcesell: Invalid argument received') + return + # Get current rate + current_rate = exchange.get_ticker(trade.pair)['bid'] + from freqtrade.main import execute_sell + execute_sell(trade, current_rate) @authorized_only @@ -403,39 +405,6 @@ def _count(bot: Bot, update: Update) -> None: logger.debug(message) send_msg(message, parse_mode=ParseMode.HTML) -@authorized_only -def _forcesellall(bot: Bot, update: Update) -> None: - """ - Handler for /forcesellall. - Sells all currently active trades - :param bot: telegram bot - :param update: message update - :return: None - """ - # 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: - for trade in trades: - # Get current rate - current_rate = exchange.get_ticker(trade.pair)['bid'] - # Get available balance - currency = trade.pair.split('_')[1] - balance = exchange.get_balance(currency) - # Execute sell - profit = trade.exec_sell_order(current_rate, balance) - message = '*{}:* Selling [{}]({}) at rate `{:f} (profit: {}%)`'.format( - trade.exchange.name, - trade.pair.replace('_', '/'), - exchange.get_pair_detail_url(trade.pair), - trade.close_rate, - round(profit, 2) - ) - logger.info(message) - send_msg(message) @authorized_only def _help(bot: Bot, update: Update) -> None: @@ -452,7 +421,7 @@ def _help(bot: Bot, update: Update) -> None: */status [table]:* `Lists all open trades` *table :* `will display trades in a table` */profit:* `Lists cumulative profit from all finished trades` -*/forcesell :* `Instantly sells the given trade, regardless of profit` +*/forcesell |all:* `Instantly sells the given trade or all trades, 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` */balance:* `Show account balance per currency` From ed34d9f22f20cb96bc144c3cf52c5ad3eebe4d25 Mon Sep 17 00:00:00 2001 From: gcarq Date: Thu, 9 Nov 2017 22:08:28 +0100 Subject: [PATCH 04/10] add tests for /forcesell all --- freqtrade/tests/test_telegram.py | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/freqtrade/tests/test_telegram.py b/freqtrade/tests/test_telegram.py index 7ca2619a7..f33ef21ff 100644 --- a/freqtrade/tests/test_telegram.py +++ b/freqtrade/tests/test_telegram.py @@ -232,6 +232,34 @@ def test_forcesell_handle(default_conf, update, ticker, mocker): assert '0.07256061 (profit: ~-0.64%)' in msg_mock.call_args_list[-1][0][0] +def test_forcesell_all_handle(default_conf, update, ticker, mocker): + mocker.patch.dict('freqtrade.main._CONF', default_conf) + mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True) + msg_mock = MagicMock() + mocker.patch.multiple('freqtrade.main.telegram', + _CONF=default_conf, + init=MagicMock(), + send_msg=msg_mock) + mocker.patch.multiple('freqtrade.main.exchange', + validate_pairs=MagicMock(), + get_ticker=ticker) + init(default_conf, 'sqlite://') + + # Create some test data + for _ in range(4): + Trade.session.add(create_trade(15.0)) + Trade.session.flush() + + msg_mock.reset_mock() + + update.message.text = '/forcesell all' + _forcesell(bot=MagicBot(), update=update) + + assert msg_mock.call_count == 4 + for args in msg_mock.call_args_list: + assert '0.07256061 (profit: ~-0.64%)' in args[0][0] + + def test_forcesell_handle_invalid(default_conf, update, mocker): mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True) @@ -265,7 +293,7 @@ def test_forcesell_handle_invalid(default_conf, update, mocker): update.message.text = '/forcesell 123456' _forcesell(bot=MagicBot(), update=update) assert msg_mock.call_count == 1 - assert 'no open trade' in msg_mock.call_args_list[0][0][0] + assert 'Invalid argument.' in msg_mock.call_args_list[0][0][0] def test_performance_handle( From c81358c2919f960aab648f5f2dd2ea561d9fc62b Mon Sep 17 00:00:00 2001 From: gcarq Date: Thu, 9 Nov 2017 22:11:02 +0100 Subject: [PATCH 05/10] remove MagicBot --- freqtrade/tests/test_telegram.py | 54 +++++++++++++++----------------- 1 file changed, 25 insertions(+), 29 deletions(-) diff --git a/freqtrade/tests/test_telegram.py b/freqtrade/tests/test_telegram.py index f33ef21ff..2729b7aed 100644 --- a/freqtrade/tests/test_telegram.py +++ b/freqtrade/tests/test_telegram.py @@ -5,7 +5,7 @@ from random import randint from unittest.mock import MagicMock import pytest -from telegram import Bot, Update, Message, Chat +from telegram import Update, Message, Chat from telegram.error import NetworkError from freqtrade.main import init, create_trade @@ -18,10 +18,6 @@ from freqtrade.rpc.telegram import ( ) -class MagicBot(MagicMock, Bot): - pass - - def test_is_enabled(default_conf, mocker): mocker.patch.dict('freqtrade.rpc.telegram._CONF', default_conf) default_conf['telegram']['enabled'] = False @@ -93,13 +89,13 @@ def test_status_handle(default_conf, update, ticker, mocker): init(default_conf, 'sqlite://') update_state(State.STOPPED) - _status(bot=MagicBot(), update=update) + _status(bot=MagicMock(), update=update) assert msg_mock.call_count == 1 assert 'trader is not running' in msg_mock.call_args_list[0][0][0] msg_mock.reset_mock() update_state(State.RUNNING) - _status(bot=MagicBot(), update=update) + _status(bot=MagicMock(), update=update) assert msg_mock.call_count == 1 assert 'no active trade' in msg_mock.call_args_list[0][0][0] msg_mock.reset_mock() @@ -111,7 +107,7 @@ def test_status_handle(default_conf, update, ticker, mocker): Trade.session.flush() # Trigger status while we have a fulfilled order for the open trade - _status(bot=MagicBot(), update=update) + _status(bot=MagicMock(), update=update) assert msg_mock.call_count == 2 assert '[BTC_ETH]' in msg_mock.call_args_list[-1][0][0] @@ -132,13 +128,13 @@ def test_status_table_handle(default_conf, update, ticker, mocker): buy=MagicMock(return_value='mocked_order_id')) init(default_conf, 'sqlite://') update_state(State.STOPPED) - _status_table(bot=MagicBot(), update=update) + _status_table(bot=MagicMock(), update=update) assert msg_mock.call_count == 1 assert 'trader is not running' in msg_mock.call_args_list[0][0][0] msg_mock.reset_mock() update_state(State.RUNNING) - _status_table(bot=MagicBot(), update=update) + _status_table(bot=MagicMock(), update=update) assert msg_mock.call_count == 1 assert 'no active order' in msg_mock.call_args_list[0][0][0] msg_mock.reset_mock() @@ -149,7 +145,7 @@ def test_status_table_handle(default_conf, update, ticker, mocker): Trade.session.add(trade) Trade.session.flush() - _status_table(bot=MagicBot(), update=update) + _status_table(bot=MagicMock(), update=update) text = re.sub('', '', msg_mock.call_args_list[-1][0][0]) line = text.split("\n") @@ -173,7 +169,7 @@ def test_profit_handle(default_conf, update, ticker, limit_buy_order, limit_sell get_ticker=ticker) init(default_conf, 'sqlite://') - _profit(bot=MagicBot(), update=update) + _profit(bot=MagicMock(), update=update) assert msg_mock.call_count == 1 assert 'no closed trade' in msg_mock.call_args_list[0][0][0] msg_mock.reset_mock() @@ -185,7 +181,7 @@ def test_profit_handle(default_conf, update, ticker, limit_buy_order, limit_sell # Simulate fulfilled LIMIT_BUY order for trade trade.update(limit_buy_order) - _profit(bot=MagicBot(), update=update) + _profit(bot=MagicMock(), update=update) assert msg_mock.call_count == 2 assert 'no closed trade' in msg_mock.call_args_list[-1][0][0] msg_mock.reset_mock() @@ -198,7 +194,7 @@ def test_profit_handle(default_conf, update, ticker, limit_buy_order, limit_sell Trade.session.add(trade) Trade.session.flush() - _profit(bot=MagicBot(), update=update) + _profit(bot=MagicMock(), update=update) assert msg_mock.call_count == 1 assert '*ROI:* `1.50701325 (10.05%)`' in msg_mock.call_args_list[-1][0][0] assert 'Best Performing:* `BTC_ETH: 10.05%`' in msg_mock.call_args_list[-1][0][0] @@ -225,7 +221,7 @@ def test_forcesell_handle(default_conf, update, ticker, mocker): Trade.session.flush() update.message.text = '/forcesell 1' - _forcesell(bot=MagicBot(), update=update) + _forcesell(bot=MagicMock(), update=update) assert msg_mock.call_count == 2 assert 'Selling [BTC/ETH]' in msg_mock.call_args_list[-1][0][0] @@ -253,7 +249,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, mocker): msg_mock.reset_mock() update.message.text = '/forcesell all' - _forcesell(bot=MagicBot(), update=update) + _forcesell(bot=MagicMock(), update=update) assert msg_mock.call_count == 4 for args in msg_mock.call_args_list: @@ -275,7 +271,7 @@ def test_forcesell_handle_invalid(default_conf, update, mocker): # Trader is not running update_state(State.STOPPED) update.message.text = '/forcesell 1' - _forcesell(bot=MagicBot(), update=update) + _forcesell(bot=MagicMock(), update=update) assert msg_mock.call_count == 1 assert 'not running' in msg_mock.call_args_list[0][0][0] @@ -283,7 +279,7 @@ def test_forcesell_handle_invalid(default_conf, update, mocker): msg_mock.reset_mock() update_state(State.RUNNING) update.message.text = '/forcesell' - _forcesell(bot=MagicBot(), update=update) + _forcesell(bot=MagicMock(), update=update) assert msg_mock.call_count == 1 assert 'Invalid argument' in msg_mock.call_args_list[0][0][0] @@ -291,7 +287,7 @@ def test_forcesell_handle_invalid(default_conf, update, mocker): msg_mock.reset_mock() update_state(State.RUNNING) update.message.text = '/forcesell 123456' - _forcesell(bot=MagicBot(), update=update) + _forcesell(bot=MagicMock(), update=update) assert msg_mock.call_count == 1 assert 'Invalid argument.' in msg_mock.call_args_list[0][0][0] @@ -325,7 +321,7 @@ def test_performance_handle( Trade.session.add(trade) Trade.session.flush() - _performance(bot=MagicBot(), update=update) + _performance(bot=MagicMock(), update=update) assert msg_mock.call_count == 2 assert 'Performance' in msg_mock.call_args_list[-1][0][0] assert 'BTC_ETH\t10.05%' in msg_mock.call_args_list[-1][0][0] @@ -346,7 +342,7 @@ def test_count_handle(default_conf, update, ticker, mocker): buy=MagicMock(return_value='mocked_order_id')) init(default_conf, 'sqlite://') update_state(State.STOPPED) - _count(bot=MagicBot(), update=update) + _count(bot=MagicMock(), update=update) assert msg_mock.call_count == 1 assert 'not running' in msg_mock.call_args_list[0][0][0] msg_mock.reset_mock() @@ -361,7 +357,7 @@ def test_count_handle(default_conf, update, ticker, mocker): Trade.session.add(trade2) Trade.session.flush() - _count(bot=MagicBot(), update=update) + _count(bot=MagicMock(), update=update) line = msg_mock.call_args_list[-1][0][0].split("\n") assert line[2] == '{}/{}'.format(2, default_conf['max_open_trades']) @@ -380,7 +376,7 @@ def test_performance_handle_invalid(default_conf, update, mocker): # Trader is not running update_state(State.STOPPED) - _performance(bot=MagicBot(), update=update) + _performance(bot=MagicMock(), update=update) assert msg_mock.call_count == 1 assert 'not running' in msg_mock.call_args_list[0][0][0] @@ -398,7 +394,7 @@ def test_start_handle(default_conf, update, mocker): init(default_conf, 'sqlite://') update_state(State.STOPPED) assert get_state() == State.STOPPED - _start(bot=MagicBot(), update=update) + _start(bot=MagicMock(), update=update) assert get_state() == State.RUNNING assert msg_mock.call_count == 0 @@ -416,7 +412,7 @@ def test_start_handle_already_running(default_conf, update, mocker): init(default_conf, 'sqlite://') update_state(State.RUNNING) assert get_state() == State.RUNNING - _start(bot=MagicBot(), update=update) + _start(bot=MagicMock(), update=update) assert get_state() == State.RUNNING assert msg_mock.call_count == 1 assert 'already running' in msg_mock.call_args_list[0][0][0] @@ -435,7 +431,7 @@ def test_stop_handle(default_conf, update, mocker): init(default_conf, 'sqlite://') update_state(State.RUNNING) assert get_state() == State.RUNNING - _stop(bot=MagicBot(), update=update) + _stop(bot=MagicMock(), update=update) assert get_state() == State.STOPPED assert msg_mock.call_count == 1 assert 'Stopping trader' in msg_mock.call_args_list[0][0][0] @@ -454,7 +450,7 @@ def test_stop_handle_already_stopped(default_conf, update, mocker): init(default_conf, 'sqlite://') update_state(State.STOPPED) assert get_state() == State.STOPPED - _stop(bot=MagicBot(), update=update) + _stop(bot=MagicMock(), update=update) assert get_state() == State.STOPPED assert msg_mock.call_count == 1 assert 'already stopped' in msg_mock.call_args_list[0][0][0] @@ -483,7 +479,7 @@ def test_balance_handle(default_conf, update, mocker): mocker.patch.multiple('freqtrade.main.exchange', get_balances=MagicMock(return_value=mock_balance)) - _balance(bot=MagicBot(), update=update) + _balance(bot=MagicMock(), update=update) assert msg_mock.call_count == 1 assert '*Currency*: BTC' in msg_mock.call_args_list[0][0][0] assert 'Balance' in msg_mock.call_args_list[0][0][0] @@ -497,7 +493,7 @@ def test_help_handle(default_conf, update, mocker): init=MagicMock(), send_msg=msg_mock) - _help(bot=MagicBot(), update=update) + _help(bot=MagicMock(), update=update) assert msg_mock.call_count == 1 assert '*/help:* `This help message`' in msg_mock.call_args_list[0][0][0] From 6ff26c561a64d0cff64b802d0ffb8793ba89ded8 Mon Sep 17 00:00:00 2001 From: gcarq Date: Thu, 9 Nov 2017 22:29:23 +0100 Subject: [PATCH 06/10] move plot_dataframe to scripts/ folder --- .coveragerc | 1 + freqtrade/analyze.py | 54 +-------------------------------------- scripts/plot_dataframe.py | 51 ++++++++++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 53 deletions(-) create mode 100755 scripts/plot_dataframe.py diff --git a/.coveragerc b/.coveragerc index 9e4dc2c18..95eea4f8f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,4 +1,5 @@ [run] omit = + scripts/* freqtrade/tests/* freqtrade/vendor/* \ No newline at end of file diff --git a/freqtrade/analyze.py b/freqtrade/analyze.py index 32bce45e0..7f504e215 100644 --- a/freqtrade/analyze.py +++ b/freqtrade/analyze.py @@ -1,13 +1,11 @@ import logging -import time from datetime import timedelta import arrow import talib.abstract as ta from pandas import DataFrame, to_datetime -from freqtrade import exchange -from freqtrade.exchange import Bittrex, get_ticker_history +from freqtrade.exchange import get_ticker_history from freqtrade.vendor.qtpylib.indicators import awesome_oscillator logging.basicConfig(level=logging.DEBUG, @@ -115,53 +113,3 @@ def get_buy_signal(pair: str) -> bool: signal = latest['buy'] == 1 logger.debug('buy_trigger: %s (pair=%s, signal=%s)', latest['date'], pair, signal) return signal - - -def plot_analyzed_dataframe(pair: str) -> None: - """ - Calls analyze() and plots the returned dataframe - :param pair: pair as str - :return: None - """ - import matplotlib - matplotlib.use("Qt5Agg") - import matplotlib.pyplot as plt - - # Init Bittrex to use public API - exchange._API = Bittrex({'key': '', 'secret': ''}) - dataframe = analyze_ticker(pair) - - # Two subplots sharing x axis - fig, (ax1, ax2, ax3) = plt.subplots(3, sharex=True) - fig.suptitle(pair, fontsize=14, fontweight='bold') - ax1.plot(dataframe.index.values, dataframe['close'], label='close') - # ax1.plot(dataframe.index.values, dataframe['sell'], 'ro', label='sell') - ax1.plot(dataframe.index.values, dataframe['sma'], '--', label='SMA') - ax1.plot(dataframe.index.values, dataframe['tema'], ':', label='TEMA') - ax1.plot(dataframe.index.values, dataframe['blower'], '-.', label='BB low') - ax1.plot(dataframe.index.values, dataframe['buy_price'], 'bo', label='buy') - ax1.legend() - - ax2.plot(dataframe.index.values, dataframe['adx'], label='ADX') - ax2.plot(dataframe.index.values, dataframe['mfi'], label='MFI') - # ax2.plot(dataframe.index.values, [25] * len(dataframe.index.values)) - ax2.legend() - - ax3.plot(dataframe.index.values, dataframe['fastk'], label='k') - ax3.plot(dataframe.index.values, dataframe['fastd'], label='d') - ax3.plot(dataframe.index.values, [20] * len(dataframe.index.values)) - ax3.legend() - - # Fine-tune figure; make subplots close to each other and hide x ticks for - # all but bottom plot. - fig.subplots_adjust(hspace=0) - plt.setp([a.get_xticklabels() for a in fig.axes[:-1]], visible=False) - plt.show() - - -if __name__ == '__main__': - # Install PYQT5==5.9 manually if you want to test this helper function - while True: - for p in ['BTC_ANT', 'BTC_ETH', 'BTC_GNT', 'BTC_ETC']: - plot_analyzed_dataframe(p) - time.sleep(60) diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py new file mode 100755 index 000000000..32d9b3cfa --- /dev/null +++ b/scripts/plot_dataframe.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 + +import matplotlib # Install PYQT5 manually if you want to test this helper function +matplotlib.use("Qt5Agg") +import matplotlib.pyplot as plt + +from freqtrade import exchange, analyze + + +def plot_analyzed_dataframe(pair: str) -> None: + """ + Calls analyze() and plots the returned dataframe + :param pair: pair as str + :return: None + """ + + # Init Bittrex to use public API + exchange._API = exchange.Bittrex({'key': '', 'secret': ''}) + dataframe = analyze.analyze_ticker(pair) + + # Two subplots sharing x axis + fig, (ax1, ax2, ax3) = plt.subplots(3, sharex=True) + fig.suptitle(pair, fontsize=14, fontweight='bold') + ax1.plot(dataframe.index.values, dataframe['close'], label='close') + # ax1.plot(dataframe.index.values, dataframe['sell'], 'ro', label='sell') + ax1.plot(dataframe.index.values, dataframe['sma'], '--', label='SMA') + ax1.plot(dataframe.index.values, dataframe['tema'], ':', label='TEMA') + ax1.plot(dataframe.index.values, dataframe['blower'], '-.', label='BB low') + ax1.plot(dataframe.index.values, dataframe['buy_price'], 'bo', label='buy') + ax1.legend() + + ax2.plot(dataframe.index.values, dataframe['adx'], label='ADX') + ax2.plot(dataframe.index.values, dataframe['mfi'], label='MFI') + # ax2.plot(dataframe.index.values, [25] * len(dataframe.index.values)) + ax2.legend() + + ax3.plot(dataframe.index.values, dataframe['fastk'], label='k') + ax3.plot(dataframe.index.values, dataframe['fastd'], label='d') + ax3.plot(dataframe.index.values, [20] * len(dataframe.index.values)) + ax3.legend() + + # Fine-tune figure; make subplots close to each other and hide x ticks for + # all but bottom plot. + fig.subplots_adjust(hspace=0) + plt.setp([a.get_xticklabels() for a in fig.axes[:-1]], visible=False) + plt.show() + + +if __name__ == '__main__': + plot_analyzed_dataframe('BTC_ETH') + From 013e13e5462843c08d1e087905ea0ee5f740929b Mon Sep 17 00:00:00 2001 From: gcarq Date: Thu, 9 Nov 2017 23:45:03 +0100 Subject: [PATCH 07/10] use tabulate for /count --- freqtrade/rpc/telegram.py | 8 ++++-- freqtrade/tests/test_telegram.py | 42 ++++++++++++++++---------------- 2 files changed, 27 insertions(+), 23 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 14478c2e3..0ad33f121 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -347,7 +347,7 @@ def _forcesell(bot: Bot, update: Update) -> None: Trade.is_open.is_(True) )).first() if not trade: - send_msg('Invalid argument. Usage: `/forcesell `') + send_msg('Invalid argument. See `/help` to view usage') logger.warning('/forcesell: Invalid argument received') return # Get current rate @@ -400,8 +400,12 @@ 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']) + message = tabulate({ + 'current': [len(trades)], + 'max': [_CONF['max_open_trades']] + }, headers=['current', 'max'], tablefmt='simple') + message = "
{}
".format(message) logger.debug(message) send_msg(message, parse_mode=ParseMode.HTML) diff --git a/freqtrade/tests/test_telegram.py b/freqtrade/tests/test_telegram.py index 2729b7aed..5e8c9a3e8 100644 --- a/freqtrade/tests/test_telegram.py +++ b/freqtrade/tests/test_telegram.py @@ -5,6 +5,7 @@ from random import randint from unittest.mock import MagicMock import pytest +from sqlalchemy import create_engine from telegram import Update, Message, Chat from telegram.error import NetworkError @@ -86,7 +87,7 @@ def test_status_handle(default_conf, update, ticker, mocker): mocker.patch.multiple('freqtrade.main.exchange', validate_pairs=MagicMock(), get_ticker=ticker) - init(default_conf, 'sqlite://') + init(default_conf, create_engine('sqlite://')) update_state(State.STOPPED) _status(bot=MagicMock(), update=update) @@ -126,7 +127,7 @@ def test_status_table_handle(default_conf, update, ticker, mocker): validate_pairs=MagicMock(), get_ticker=ticker, buy=MagicMock(return_value='mocked_order_id')) - init(default_conf, 'sqlite://') + init(default_conf, create_engine('sqlite://')) update_state(State.STOPPED) _status_table(bot=MagicMock(), update=update) assert msg_mock.call_count == 1 @@ -167,7 +168,7 @@ def test_profit_handle(default_conf, update, ticker, limit_buy_order, limit_sell mocker.patch.multiple('freqtrade.main.exchange', validate_pairs=MagicMock(), get_ticker=ticker) - init(default_conf, 'sqlite://') + init(default_conf, create_engine('sqlite://')) _profit(bot=MagicMock(), update=update) assert msg_mock.call_count == 1 @@ -211,7 +212,7 @@ def test_forcesell_handle(default_conf, update, ticker, mocker): mocker.patch.multiple('freqtrade.main.exchange', validate_pairs=MagicMock(), get_ticker=ticker) - init(default_conf, 'sqlite://') + init(default_conf, create_engine('sqlite://')) # Create some test data trade = create_trade(15.0) @@ -239,7 +240,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, mocker): mocker.patch.multiple('freqtrade.main.exchange', validate_pairs=MagicMock(), get_ticker=ticker) - init(default_conf, 'sqlite://') + init(default_conf, create_engine('sqlite://')) # Create some test data for _ in range(4): @@ -266,7 +267,7 @@ def test_forcesell_handle_invalid(default_conf, update, mocker): send_msg=msg_mock) mocker.patch.multiple('freqtrade.main.exchange', validate_pairs=MagicMock()) - init(default_conf, 'sqlite://') + init(default_conf, create_engine('sqlite://')) # Trader is not running update_state(State.STOPPED) @@ -304,7 +305,7 @@ def test_performance_handle( mocker.patch.multiple('freqtrade.main.exchange', validate_pairs=MagicMock(), get_ticker=ticker) - init(default_conf, 'sqlite://') + init(default_conf, create_engine('sqlite://')) # Create some test data trade = create_trade(15.0) @@ -340,7 +341,7 @@ def test_count_handle(default_conf, update, ticker, mocker): validate_pairs=MagicMock(), get_ticker=ticker, buy=MagicMock(return_value='mocked_order_id')) - init(default_conf, 'sqlite://') + init(default_conf, create_engine('sqlite://')) update_state(State.STOPPED) _count(bot=MagicMock(), update=update) assert msg_mock.call_count == 1 @@ -349,17 +350,16 @@ def test_count_handle(default_conf, update, ticker, mocker): update_state(State.RUNNING) # 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.add(create_trade(15.0)) Trade.session.flush() + msg_mock.reset_mock() _count(bot=MagicMock(), update=update) - line = msg_mock.call_args_list[-1][0][0].split("\n") - assert line[2] == '{}/{}'.format(2, default_conf['max_open_trades']) + + msg = '
  current    max\n---------  -----\n        1      {}
'.format( + default_conf['max_open_trades'] + ) + assert msg in msg_mock.call_args_list[0][0][0] def test_performance_handle_invalid(default_conf, update, mocker): @@ -372,7 +372,7 @@ def test_performance_handle_invalid(default_conf, update, mocker): send_msg=msg_mock) mocker.patch.multiple('freqtrade.main.exchange', validate_pairs=MagicMock()) - init(default_conf, 'sqlite://') + init(default_conf, create_engine('sqlite://')) # Trader is not running update_state(State.STOPPED) @@ -391,7 +391,7 @@ def test_start_handle(default_conf, update, mocker): mocker.patch.multiple('freqtrade.main.exchange', _CONF=default_conf, init=MagicMock()) - init(default_conf, 'sqlite://') + init(default_conf, create_engine('sqlite://')) update_state(State.STOPPED) assert get_state() == State.STOPPED _start(bot=MagicMock(), update=update) @@ -409,7 +409,7 @@ def test_start_handle_already_running(default_conf, update, mocker): mocker.patch.multiple('freqtrade.main.exchange', _CONF=default_conf, init=MagicMock()) - init(default_conf, 'sqlite://') + init(default_conf, create_engine('sqlite://')) update_state(State.RUNNING) assert get_state() == State.RUNNING _start(bot=MagicMock(), update=update) @@ -428,7 +428,7 @@ def test_stop_handle(default_conf, update, mocker): mocker.patch.multiple('freqtrade.main.exchange', _CONF=default_conf, init=MagicMock()) - init(default_conf, 'sqlite://') + init(default_conf, create_engine('sqlite://')) update_state(State.RUNNING) assert get_state() == State.RUNNING _stop(bot=MagicMock(), update=update) @@ -447,7 +447,7 @@ def test_stop_handle_already_stopped(default_conf, update, mocker): mocker.patch.multiple('freqtrade.main.exchange', _CONF=default_conf, init=MagicMock()) - init(default_conf, 'sqlite://') + init(default_conf, create_engine('sqlite://')) update_state(State.STOPPED) assert get_state() == State.STOPPED _stop(bot=MagicMock(), update=update) From 98f11fc7bbba14c64ce32bf2da9a9ceb2f68dfbf Mon Sep 17 00:00:00 2001 From: gcarq Date: Thu, 9 Nov 2017 23:45:22 +0100 Subject: [PATCH 08/10] fix sqlite threading issue --- freqtrade/persistence.py | 16 ++++++++++------ freqtrade/tests/test_main.py | 15 ++++++++------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index b5ec4c0ec..8c23cf713 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -5,9 +5,11 @@ from typing import Optional, Dict import arrow from sqlalchemy import Boolean, Column, DateTime, Float, Integer, String, create_engine +from sqlalchemy.engine import Engine from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm.scoping import scoped_session from sqlalchemy.orm.session import sessionmaker +from sqlalchemy.pool import StaticPool logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') @@ -17,23 +19,25 @@ _CONF = {} _DECL_BASE = declarative_base() -def init(config: dict, db_url: Optional[str] = None) -> None: +def init(config: dict, engine: Optional[Engine] = None) -> None: """ Initializes this module with the given config, registers all known command handlers and starts polling for message updates :param config: config to use - :param db_url: database connector string for sqlalchemy (Optional) + :param engine: database engine for sqlalchemy (Optional) :return: None """ _CONF.update(config) - if not db_url: + if not engine: if _CONF.get('dry_run', False): - db_url = 'sqlite://' + engine = create_engine('sqlite://', + connect_args={'check_same_thread': False}, + poolclass=StaticPool, + echo=False) else: - db_url = 'sqlite:///tradesv3.sqlite' + engine = create_engine('sqlite:///tradesv3.sqlite') - engine = create_engine(db_url, echo=False) session = scoped_session(sessionmaker(bind=engine, autoflush=True, autocommit=True)) Trade.session = session() Trade.query = session.query_property() diff --git a/freqtrade/tests/test_main.py b/freqtrade/tests/test_main.py index 151ecaabc..f114b4dde 100644 --- a/freqtrade/tests/test_main.py +++ b/freqtrade/tests/test_main.py @@ -4,6 +4,7 @@ from unittest.mock import MagicMock import pytest import requests +from sqlalchemy import create_engine from freqtrade.exchange import Exchanges from freqtrade.main import create_trade, handle_trade, close_trade_if_fulfilled, init, \ @@ -20,7 +21,7 @@ def test_process_trade_creation(default_conf, ticker, mocker): validate_pairs=MagicMock(), get_ticker=ticker, buy=MagicMock(return_value='mocked_limit_buy')) - init(default_conf, 'sqlite://') + init(default_conf, create_engine('sqlite://')) trades = Trade.query.filter(Trade.is_open.is_(True)).all() assert len(trades) == 0 @@ -49,7 +50,7 @@ def test_process_exchange_failures(default_conf, ticker, mocker): validate_pairs=MagicMock(), get_ticker=ticker, buy=MagicMock(side_effect=requests.exceptions.RequestException)) - init(default_conf, 'sqlite://') + init(default_conf, create_engine('sqlite://')) result = _process() assert result is False assert sleep_mock.has_calls() @@ -64,7 +65,7 @@ def test_process_runtime_error(default_conf, ticker, mocker): validate_pairs=MagicMock(), get_ticker=ticker, buy=MagicMock(side_effect=RuntimeError)) - init(default_conf, 'sqlite://') + init(default_conf, create_engine('sqlite://')) assert get_state() == State.RUNNING result = _process() @@ -82,7 +83,7 @@ def test_process_trade_handling(default_conf, ticker, limit_buy_order, mocker): get_ticker=ticker, buy=MagicMock(return_value='mocked_limit_buy'), get_order=MagicMock(return_value=limit_buy_order)) - init(default_conf, 'sqlite://') + init(default_conf, create_engine('sqlite://')) trades = Trade.query.filter(Trade.is_open.is_(True)).all() assert len(trades) == 0 @@ -106,7 +107,7 @@ def test_create_trade(default_conf, ticker, limit_buy_order, mocker): # Save state of current whitelist whitelist = copy.deepcopy(default_conf['exchange']['pair_whitelist']) - init(default_conf, 'sqlite://') + init(default_conf, create_engine('sqlite://')) trade = create_trade(15.0) Trade.session.add(trade) Trade.session.flush() @@ -167,7 +168,7 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, mocker): }), buy=MagicMock(return_value='mocked_limit_buy'), sell=MagicMock(return_value='mocked_limit_sell')) - init(default_conf, 'sqlite://') + init(default_conf, create_engine('sqlite://')) trade = create_trade(15.0) trade.update(limit_buy_order) Trade.session.add(trade) @@ -197,7 +198,7 @@ def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, mo buy=MagicMock(return_value='mocked_limit_buy')) # Create trade and sell it - init(default_conf, 'sqlite://') + init(default_conf, create_engine('sqlite://')) trade = create_trade(15.0) trade.update(limit_buy_order) trade.update(limit_sell_order) From f7a27c156cfdf8c8111a8545d897e9843077c557 Mon Sep 17 00:00:00 2001 From: gcarq Date: Thu, 9 Nov 2017 23:51:32 +0100 Subject: [PATCH 09/10] add /version command handler --- freqtrade/rpc/telegram.py | 16 +++++++++++++++- freqtrade/tests/test_telegram.py | 18 ++++++++++++++++-- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 0ad33f121..a3d700b7e 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -11,7 +11,7 @@ from telegram import ParseMode, Bot, Update from telegram.error import NetworkError from telegram.ext import CommandHandler, Updater -from freqtrade import exchange +from freqtrade import exchange, __version__ from freqtrade.misc import get_state, State, update_state from freqtrade.persistence import Trade @@ -51,6 +51,7 @@ def init(config: dict) -> None: CommandHandler('performance', _performance), CommandHandler('count', _count), CommandHandler('help', _help), + CommandHandler('version', _version), ] for handle in handles: _UPDATER.dispatcher.add_handler(handle) @@ -430,10 +431,23 @@ def _help(bot: Bot, update: Update) -> None: */count:* `Show number of trades running compared to allowed number of trades` */balance:* `Show account balance per currency` */help:* `This help message` +*/version:* `Show version` """ send_msg(message, bot=bot) +@authorized_only +def _version(bot: Bot, update: Update) -> None: + """ + Handler for /version. + Show version information + :param bot: telegram bot + :param update: message update + :return: None + """ + send_msg('*Version:* `{}`'.format(__version__), bot=bot) + + def shorten_date(date): """ Trim the date so it fits on small screens diff --git a/freqtrade/tests/test_telegram.py b/freqtrade/tests/test_telegram.py index 5e8c9a3e8..795901ced 100644 --- a/freqtrade/tests/test_telegram.py +++ b/freqtrade/tests/test_telegram.py @@ -9,14 +9,15 @@ from sqlalchemy import create_engine from telegram import Update, Message, Chat from telegram.error import NetworkError +from freqtrade import __version__ from freqtrade.main import init, create_trade from freqtrade.misc import update_state, State, get_state from freqtrade.persistence import Trade from freqtrade.rpc import telegram from freqtrade.rpc.telegram import ( _status, _status_table, _profit, _forcesell, _performance, _count, _start, _stop, _balance, - authorized_only, _help, is_enabled, send_msg -) + authorized_only, _help, is_enabled, send_msg, + _version) def test_is_enabled(default_conf, mocker): @@ -498,6 +499,19 @@ def test_help_handle(default_conf, update, mocker): assert '*/help:* `This help message`' in msg_mock.call_args_list[0][0][0] +def test_version_handle(default_conf, update, mocker): + mocker.patch.dict('freqtrade.main._CONF', default_conf) + msg_mock = MagicMock() + mocker.patch.multiple('freqtrade.main.telegram', + _CONF=default_conf, + init=MagicMock(), + send_msg=msg_mock) + + _version(bot=MagicMock(), update=update) + assert msg_mock.call_count == 1 + assert '*Version:* `{}`'.format(__version__) in msg_mock.call_args_list[0][0][0] + + def test_send_msg(default_conf, mocker): mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch.multiple('freqtrade.main.telegram', From 0de211674d497210d50064d4156ded82beb63213 Mon Sep 17 00:00:00 2001 From: gcarq Date: Thu, 9 Nov 2017 23:52:34 +0100 Subject: [PATCH 10/10] version bump --- freqtrade/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index 3edb37de3..b3d4bd950 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,3 +1,3 @@ -__version__ = '0.14.0' +__version__ = '0.14.1' from . import main