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/__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 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/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/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/rpc/telegram.py b/freqtrade/rpc/telegram.py index bbdece23b..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) @@ -331,26 +332,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: - send_msg('Invalid argument. Usage: `/forcesell `') + # 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. See `/help` to view usage') 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 @@ -397,8 +401,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) @@ -418,15 +426,28 @@ 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` */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_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_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) diff --git a/freqtrade/tests/test_telegram.py b/freqtrade/tests/test_telegram.py index 0fc79ecad..795901ced 100644 --- a/freqtrade/tests/test_telegram.py +++ b/freqtrade/tests/test_telegram.py @@ -5,21 +5,19 @@ from random import randint from unittest.mock import MagicMock import pytest -from telegram import Bot, Update, Message, Chat +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 -) - - -class MagicBot(MagicMock, Bot): - pass + authorized_only, _help, is_enabled, send_msg, + _version) def test_is_enabled(default_conf, mocker): @@ -90,16 +88,16 @@ 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=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 +109,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] @@ -130,15 +128,15 @@ 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=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 +147,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") @@ -171,9 +169,9 @@ 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=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 +183,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 +196,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] @@ -215,7 +213,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) @@ -225,13 +223,41 @@ 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] 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, create_engine('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=MagicMock(), 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) @@ -242,12 +268,12 @@ 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) 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] @@ -255,7 +281,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] @@ -263,12 +289,13 @@ 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 '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(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() @@ -279,7 +306,7 @@ def test_performance_handle(default_conf, update, ticker, limit_buy_order, limit 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) @@ -296,7 +323,7 @@ def test_performance_handle(default_conf, update, ticker, limit_buy_order, limit 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] @@ -315,26 +342,25 @@ 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=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() 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() - _count(bot=MagicBot(), update=update) - line = msg_mock.call_args_list[-1][0][0].split("\n") - assert line[2] == '{}/{}'.format(2, default_conf['max_open_trades']) + msg_mock.reset_mock() + _count(bot=MagicMock(), update=update) + + 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): @@ -347,11 +373,11 @@ 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) - _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] @@ -366,10 +392,10 @@ 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=MagicBot(), update=update) + _start(bot=MagicMock(), update=update) assert get_state() == State.RUNNING assert msg_mock.call_count == 0 @@ -384,10 +410,10 @@ 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=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] @@ -403,10 +429,10 @@ 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=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] @@ -422,10 +448,10 @@ 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=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] @@ -454,7 +480,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] @@ -468,11 +494,24 @@ 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] +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', 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') +