Merge branch 'release/0.14.1'

This commit is contained in:
gcarq 2017-11-09 23:53:10 +01:00
commit de13df6ede
10 changed files with 244 additions and 140 deletions

View File

@ -1,4 +1,5 @@
[run] [run]
omit = omit =
scripts/*
freqtrade/tests/* freqtrade/tests/*
freqtrade/vendor/* freqtrade/vendor/*

View File

@ -1,3 +1,3 @@
__version__ = '0.14.0' __version__ = '0.14.1'
from . import main from . import main

View File

@ -1,13 +1,11 @@
import logging import logging
import time
from datetime import timedelta from datetime import timedelta
import arrow import arrow
import talib.abstract as ta import talib.abstract as ta
from pandas import DataFrame, to_datetime from pandas import DataFrame, to_datetime
from freqtrade import exchange from freqtrade.exchange import get_ticker_history
from freqtrade.exchange import Bittrex, get_ticker_history
from freqtrade.vendor.qtpylib.indicators import awesome_oscillator from freqtrade.vendor.qtpylib.indicators import awesome_oscillator
logging.basicConfig(level=logging.DEBUG, logging.basicConfig(level=logging.DEBUG,
@ -115,53 +113,3 @@ def get_buy_signal(pair: str) -> bool:
signal = latest['buy'] == 1 signal = latest['buy'] == 1
logger.debug('buy_trigger: %s (pair=%s, signal=%s)', latest['date'], pair, signal) logger.debug('buy_trigger: %s (pair=%s, signal=%s)', latest['date'], pair, signal)
return 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)

View File

@ -63,7 +63,12 @@ def validate_pairs(pairs: List[str]) -> None:
:return: None :return: None
""" """
markets = _API.get_markets() markets = _API.get_markets()
stake_cur = _CONF['stake_currency']
for pair in pairs: 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: if pair not in markets:
raise RuntimeError('Pair {} is not available at {}'.format(pair, _API.name.lower())) raise RuntimeError('Pair {} is not available at {}'.format(pair, _API.name.lower()))

View File

@ -5,9 +5,11 @@ from typing import Optional, Dict
import arrow import arrow
from sqlalchemy import Boolean, Column, DateTime, Float, Integer, String, create_engine 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.ext.declarative import declarative_base
from sqlalchemy.orm.scoping import scoped_session from sqlalchemy.orm.scoping import scoped_session
from sqlalchemy.orm.session import sessionmaker from sqlalchemy.orm.session import sessionmaker
from sqlalchemy.pool import StaticPool
logging.basicConfig(level=logging.DEBUG, logging.basicConfig(level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
@ -17,23 +19,25 @@ _CONF = {}
_DECL_BASE = declarative_base() _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, Initializes this module with the given config,
registers all known command handlers registers all known command handlers
and starts polling for message updates and starts polling for message updates
:param config: config to use :param config: config to use
:param db_url: database connector string for sqlalchemy (Optional) :param engine: database engine for sqlalchemy (Optional)
:return: None :return: None
""" """
_CONF.update(config) _CONF.update(config)
if not db_url: if not engine:
if _CONF.get('dry_run', False): if _CONF.get('dry_run', False):
db_url = 'sqlite://' engine = create_engine('sqlite://',
connect_args={'check_same_thread': False},
poolclass=StaticPool,
echo=False)
else: 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)) session = scoped_session(sessionmaker(bind=engine, autoflush=True, autocommit=True))
Trade.session = session() Trade.session = session()
Trade.query = session.query_property() Trade.query = session.query_property()

View File

@ -11,7 +11,7 @@ from telegram import ParseMode, Bot, Update
from telegram.error import NetworkError from telegram.error import NetworkError
from telegram.ext import CommandHandler, Updater 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.misc import get_state, State, update_state
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
@ -51,6 +51,7 @@ def init(config: dict) -> None:
CommandHandler('performance', _performance), CommandHandler('performance', _performance),
CommandHandler('count', _count), CommandHandler('count', _count),
CommandHandler('help', _help), CommandHandler('help', _help),
CommandHandler('version', _version),
] ]
for handle in handles: for handle in handles:
_UPDATER.dispatcher.add_handler(handle) _UPDATER.dispatcher.add_handler(handle)
@ -331,26 +332,29 @@ def _forcesell(bot: Bot, update: Update) -> None:
send_msg('`trader is not running`', bot=bot) send_msg('`trader is not running`', bot=bot)
return return
try: trade_id = update.message.text.replace('/forcesell', '').strip()
trade_id = int(update.message.text if trade_id == 'all':
.replace('/forcesell', '') # Execute sell for all open orders
.strip()) for trade in Trade.query.filter(Trade.is_open.is_(True)).all():
# Query for trade # Get current rate
trade = Trade.query.filter(and_( current_rate = exchange.get_ticker(trade.pair)['bid']
Trade.id == trade_id, from freqtrade.main import execute_sell
Trade.is_open.is_(True) execute_sell(trade, current_rate)
)).first() return
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)
except ValueError: # Query for trade
send_msg('Invalid argument. Usage: `/forcesell <trade_id>`') 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') 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 @authorized_only
@ -397,8 +401,12 @@ def _count(bot: Bot, update: Update) -> None:
return return
trades = Trade.query.filter(Trade.is_open.is_(True)).all() trades = Trade.query.filter(Trade.is_open.is_(True)).all()
message = '<b>Count:</b>\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 = "<pre>{}</pre>".format(message)
logger.debug(message) logger.debug(message)
send_msg(message, parse_mode=ParseMode.HTML) send_msg(message, parse_mode=ParseMode.HTML)
@ -418,15 +426,28 @@ def _help(bot: Bot, update: Update) -> None:
*/status [table]:* `Lists all open trades` */status [table]:* `Lists all open trades`
*table :* `will display trades in a table` *table :* `will display trades in a table`
*/profit:* `Lists cumulative profit from all finished trades` */profit:* `Lists cumulative profit from all finished trades`
*/forcesell <trade_id>:* `Instantly sells the given trade, regardless of profit` */forcesell <trade_id>|all:* `Instantly sells the given trade or all trades, regardless of profit`
*/performance:* `Show performance of each finished trade grouped by pair` */performance:* `Show performance of each finished trade grouped by pair`
*/count:* `Show number of trades running compared to allowed number of trades` */count:* `Show number of trades running compared to allowed number of trades`
*/balance:* `Show account balance per currency` */balance:* `Show account balance per currency`
*/help:* `This help message` */help:* `This help message`
*/version:* `Show version`
""" """
send_msg(message, bot=bot) 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): def shorten_date(date):
""" """
Trim the date so it fits on small screens Trim the date so it fits on small screens

View File

@ -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'])

View File

@ -4,6 +4,7 @@ from unittest.mock import MagicMock
import pytest import pytest
import requests import requests
from sqlalchemy import create_engine
from freqtrade.exchange import Exchanges from freqtrade.exchange import Exchanges
from freqtrade.main import create_trade, handle_trade, close_trade_if_fulfilled, init, \ 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(), validate_pairs=MagicMock(),
get_ticker=ticker, get_ticker=ticker,
buy=MagicMock(return_value='mocked_limit_buy')) 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() trades = Trade.query.filter(Trade.is_open.is_(True)).all()
assert len(trades) == 0 assert len(trades) == 0
@ -49,7 +50,7 @@ def test_process_exchange_failures(default_conf, ticker, mocker):
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
get_ticker=ticker, get_ticker=ticker,
buy=MagicMock(side_effect=requests.exceptions.RequestException)) buy=MagicMock(side_effect=requests.exceptions.RequestException))
init(default_conf, 'sqlite://') init(default_conf, create_engine('sqlite://'))
result = _process() result = _process()
assert result is False assert result is False
assert sleep_mock.has_calls() assert sleep_mock.has_calls()
@ -64,7 +65,7 @@ def test_process_runtime_error(default_conf, ticker, mocker):
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
get_ticker=ticker, get_ticker=ticker,
buy=MagicMock(side_effect=RuntimeError)) buy=MagicMock(side_effect=RuntimeError))
init(default_conf, 'sqlite://') init(default_conf, create_engine('sqlite://'))
assert get_state() == State.RUNNING assert get_state() == State.RUNNING
result = _process() result = _process()
@ -82,7 +83,7 @@ def test_process_trade_handling(default_conf, ticker, limit_buy_order, mocker):
get_ticker=ticker, get_ticker=ticker,
buy=MagicMock(return_value='mocked_limit_buy'), buy=MagicMock(return_value='mocked_limit_buy'),
get_order=MagicMock(return_value=limit_buy_order)) 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() trades = Trade.query.filter(Trade.is_open.is_(True)).all()
assert len(trades) == 0 assert len(trades) == 0
@ -106,7 +107,7 @@ def test_create_trade(default_conf, ticker, limit_buy_order, mocker):
# Save state of current whitelist # Save state of current whitelist
whitelist = copy.deepcopy(default_conf['exchange']['pair_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 = create_trade(15.0)
Trade.session.add(trade) Trade.session.add(trade)
Trade.session.flush() 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'), buy=MagicMock(return_value='mocked_limit_buy'),
sell=MagicMock(return_value='mocked_limit_sell')) sell=MagicMock(return_value='mocked_limit_sell'))
init(default_conf, 'sqlite://') init(default_conf, create_engine('sqlite://'))
trade = create_trade(15.0) trade = create_trade(15.0)
trade.update(limit_buy_order) trade.update(limit_buy_order)
Trade.session.add(trade) 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')) buy=MagicMock(return_value='mocked_limit_buy'))
# Create trade and sell it # Create trade and sell it
init(default_conf, 'sqlite://') init(default_conf, create_engine('sqlite://'))
trade = create_trade(15.0) trade = create_trade(15.0)
trade.update(limit_buy_order) trade.update(limit_buy_order)
trade.update(limit_sell_order) trade.update(limit_sell_order)

View File

@ -5,21 +5,19 @@ from random import randint
from unittest.mock import MagicMock from unittest.mock import MagicMock
import pytest 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 telegram.error import NetworkError
from freqtrade import __version__
from freqtrade.main import init, create_trade from freqtrade.main import init, create_trade
from freqtrade.misc import update_state, State, get_state from freqtrade.misc import update_state, State, get_state
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.rpc import telegram from freqtrade.rpc import telegram
from freqtrade.rpc.telegram import ( from freqtrade.rpc.telegram import (
_status, _status_table, _profit, _forcesell, _performance, _count, _start, _stop, _balance, _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)
class MagicBot(MagicMock, Bot):
pass
def test_is_enabled(default_conf, mocker): 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', mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
get_ticker=ticker) get_ticker=ticker)
init(default_conf, 'sqlite://') init(default_conf, create_engine('sqlite://'))
update_state(State.STOPPED) update_state(State.STOPPED)
_status(bot=MagicBot(), update=update) _status(bot=MagicMock(), update=update)
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert 'trader is not running' in msg_mock.call_args_list[0][0][0] assert 'trader is not running' in msg_mock.call_args_list[0][0][0]
msg_mock.reset_mock() msg_mock.reset_mock()
update_state(State.RUNNING) update_state(State.RUNNING)
_status(bot=MagicBot(), update=update) _status(bot=MagicMock(), update=update)
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert 'no active trade' in msg_mock.call_args_list[0][0][0] assert 'no active trade' in msg_mock.call_args_list[0][0][0]
msg_mock.reset_mock() msg_mock.reset_mock()
@ -111,7 +109,7 @@ def test_status_handle(default_conf, update, ticker, mocker):
Trade.session.flush() Trade.session.flush()
# Trigger status while we have a fulfilled order for the open trade # 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 msg_mock.call_count == 2
assert '[BTC_ETH]' in msg_mock.call_args_list[-1][0][0] 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(), validate_pairs=MagicMock(),
get_ticker=ticker, get_ticker=ticker,
buy=MagicMock(return_value='mocked_order_id')) buy=MagicMock(return_value='mocked_order_id'))
init(default_conf, 'sqlite://') init(default_conf, create_engine('sqlite://'))
update_state(State.STOPPED) update_state(State.STOPPED)
_status_table(bot=MagicBot(), update=update) _status_table(bot=MagicMock(), update=update)
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert 'trader is not running' in msg_mock.call_args_list[0][0][0] assert 'trader is not running' in msg_mock.call_args_list[0][0][0]
msg_mock.reset_mock() msg_mock.reset_mock()
update_state(State.RUNNING) update_state(State.RUNNING)
_status_table(bot=MagicBot(), update=update) _status_table(bot=MagicMock(), update=update)
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert 'no active order' in msg_mock.call_args_list[0][0][0] assert 'no active order' in msg_mock.call_args_list[0][0][0]
msg_mock.reset_mock() msg_mock.reset_mock()
@ -149,7 +147,7 @@ def test_status_table_handle(default_conf, update, ticker, mocker):
Trade.session.add(trade) Trade.session.add(trade)
Trade.session.flush() Trade.session.flush()
_status_table(bot=MagicBot(), update=update) _status_table(bot=MagicMock(), update=update)
text = re.sub('</?pre>', '', msg_mock.call_args_list[-1][0][0]) text = re.sub('</?pre>', '', msg_mock.call_args_list[-1][0][0])
line = text.split("\n") 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', mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
get_ticker=ticker) 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 msg_mock.call_count == 1
assert 'no closed trade' in msg_mock.call_args_list[0][0][0] assert 'no closed trade' in msg_mock.call_args_list[0][0][0]
msg_mock.reset_mock() 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 # Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order) trade.update(limit_buy_order)
_profit(bot=MagicBot(), update=update) _profit(bot=MagicMock(), update=update)
assert msg_mock.call_count == 2 assert msg_mock.call_count == 2
assert 'no closed trade' in msg_mock.call_args_list[-1][0][0] assert 'no closed trade' in msg_mock.call_args_list[-1][0][0]
msg_mock.reset_mock() 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.add(trade)
Trade.session.flush() Trade.session.flush()
_profit(bot=MagicBot(), update=update) _profit(bot=MagicMock(), update=update)
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert '*ROI:* `1.50701325 (10.05%)`' in msg_mock.call_args_list[-1][0][0] 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] 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', mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
get_ticker=ticker) get_ticker=ticker)
init(default_conf, 'sqlite://') init(default_conf, create_engine('sqlite://'))
# Create some test data # Create some test data
trade = create_trade(15.0) trade = create_trade(15.0)
@ -225,13 +223,41 @@ def test_forcesell_handle(default_conf, update, ticker, mocker):
Trade.session.flush() Trade.session.flush()
update.message.text = '/forcesell 1' update.message.text = '/forcesell 1'
_forcesell(bot=MagicBot(), update=update) _forcesell(bot=MagicMock(), update=update)
assert msg_mock.call_count == 2 assert msg_mock.call_count == 2
assert 'Selling [BTC/ETH]' in msg_mock.call_args_list[-1][0][0] 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] 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): def test_forcesell_handle_invalid(default_conf, update, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True) 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) send_msg=msg_mock)
mocker.patch.multiple('freqtrade.main.exchange', mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock()) validate_pairs=MagicMock())
init(default_conf, 'sqlite://') init(default_conf, create_engine('sqlite://'))
# Trader is not running # Trader is not running
update_state(State.STOPPED) update_state(State.STOPPED)
update.message.text = '/forcesell 1' update.message.text = '/forcesell 1'
_forcesell(bot=MagicBot(), update=update) _forcesell(bot=MagicMock(), update=update)
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert 'not running' in msg_mock.call_args_list[0][0][0] 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() msg_mock.reset_mock()
update_state(State.RUNNING) update_state(State.RUNNING)
update.message.text = '/forcesell' update.message.text = '/forcesell'
_forcesell(bot=MagicBot(), update=update) _forcesell(bot=MagicMock(), update=update)
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert 'Invalid argument' in msg_mock.call_args_list[0][0][0] 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() msg_mock.reset_mock()
update_state(State.RUNNING) update_state(State.RUNNING)
update.message.text = '/forcesell 123456' update.message.text = '/forcesell 123456'
_forcesell(bot=MagicBot(), update=update) _forcesell(bot=MagicMock(), update=update)
assert msg_mock.call_count == 1 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.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True) mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
msg_mock = MagicMock() 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', mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
get_ticker=ticker) get_ticker=ticker)
init(default_conf, 'sqlite://') init(default_conf, create_engine('sqlite://'))
# Create some test data # Create some test data
trade = create_trade(15.0) 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.add(trade)
Trade.session.flush() Trade.session.flush()
_performance(bot=MagicBot(), update=update) _performance(bot=MagicMock(), update=update)
assert msg_mock.call_count == 2 assert msg_mock.call_count == 2
assert 'Performance' in msg_mock.call_args_list[-1][0][0] assert 'Performance' in msg_mock.call_args_list[-1][0][0]
assert '<code>BTC_ETH\t10.05%</code>' in msg_mock.call_args_list[-1][0][0] assert '<code>BTC_ETH\t10.05%</code>' 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(), validate_pairs=MagicMock(),
get_ticker=ticker, get_ticker=ticker,
buy=MagicMock(return_value='mocked_order_id')) buy=MagicMock(return_value='mocked_order_id'))
init(default_conf, 'sqlite://') init(default_conf, create_engine('sqlite://'))
update_state(State.STOPPED) update_state(State.STOPPED)
_count(bot=MagicBot(), update=update) _count(bot=MagicMock(), update=update)
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert 'not running' in msg_mock.call_args_list[0][0][0] assert 'not running' in msg_mock.call_args_list[0][0][0]
msg_mock.reset_mock() msg_mock.reset_mock()
update_state(State.RUNNING) update_state(State.RUNNING)
# Create some test data # Create some test data
trade = create_trade(15.0) Trade.session.add(create_trade(15.0))
trade2 = create_trade(15.0)
assert trade
assert trade2
Trade.session.add(trade)
Trade.session.add(trade2)
Trade.session.flush() Trade.session.flush()
_count(bot=MagicBot(), update=update) msg_mock.reset_mock()
line = msg_mock.call_args_list[-1][0][0].split("\n") _count(bot=MagicMock(), update=update)
assert line[2] == '{}/{}'.format(2, default_conf['max_open_trades'])
msg = '<pre> current max\n--------- -----\n 1 {}</pre>'.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): 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) send_msg=msg_mock)
mocker.patch.multiple('freqtrade.main.exchange', mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock()) validate_pairs=MagicMock())
init(default_conf, 'sqlite://') init(default_conf, create_engine('sqlite://'))
# Trader is not running # Trader is not running
update_state(State.STOPPED) update_state(State.STOPPED)
_performance(bot=MagicBot(), update=update) _performance(bot=MagicMock(), update=update)
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert 'not running' in msg_mock.call_args_list[0][0][0] 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', mocker.patch.multiple('freqtrade.main.exchange',
_CONF=default_conf, _CONF=default_conf,
init=MagicMock()) init=MagicMock())
init(default_conf, 'sqlite://') init(default_conf, create_engine('sqlite://'))
update_state(State.STOPPED) update_state(State.STOPPED)
assert get_state() == State.STOPPED assert get_state() == State.STOPPED
_start(bot=MagicBot(), update=update) _start(bot=MagicMock(), update=update)
assert get_state() == State.RUNNING assert get_state() == State.RUNNING
assert msg_mock.call_count == 0 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', mocker.patch.multiple('freqtrade.main.exchange',
_CONF=default_conf, _CONF=default_conf,
init=MagicMock()) init=MagicMock())
init(default_conf, 'sqlite://') init(default_conf, create_engine('sqlite://'))
update_state(State.RUNNING) update_state(State.RUNNING)
assert get_state() == State.RUNNING assert get_state() == State.RUNNING
_start(bot=MagicBot(), update=update) _start(bot=MagicMock(), update=update)
assert get_state() == State.RUNNING assert get_state() == State.RUNNING
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert 'already running' in msg_mock.call_args_list[0][0][0] 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', mocker.patch.multiple('freqtrade.main.exchange',
_CONF=default_conf, _CONF=default_conf,
init=MagicMock()) init=MagicMock())
init(default_conf, 'sqlite://') init(default_conf, create_engine('sqlite://'))
update_state(State.RUNNING) update_state(State.RUNNING)
assert get_state() == State.RUNNING assert get_state() == State.RUNNING
_stop(bot=MagicBot(), update=update) _stop(bot=MagicMock(), update=update)
assert get_state() == State.STOPPED assert get_state() == State.STOPPED
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert 'Stopping trader' in msg_mock.call_args_list[0][0][0] 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', mocker.patch.multiple('freqtrade.main.exchange',
_CONF=default_conf, _CONF=default_conf,
init=MagicMock()) init=MagicMock())
init(default_conf, 'sqlite://') init(default_conf, create_engine('sqlite://'))
update_state(State.STOPPED) update_state(State.STOPPED)
assert get_state() == State.STOPPED assert get_state() == State.STOPPED
_stop(bot=MagicBot(), update=update) _stop(bot=MagicMock(), update=update)
assert get_state() == State.STOPPED assert get_state() == State.STOPPED
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert 'already stopped' in msg_mock.call_args_list[0][0][0] 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', mocker.patch.multiple('freqtrade.main.exchange',
get_balances=MagicMock(return_value=mock_balance)) get_balances=MagicMock(return_value=mock_balance))
_balance(bot=MagicBot(), update=update) _balance(bot=MagicMock(), update=update)
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert '*Currency*: BTC' in msg_mock.call_args_list[0][0][0] assert '*Currency*: BTC' in msg_mock.call_args_list[0][0][0]
assert 'Balance' 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(), init=MagicMock(),
send_msg=msg_mock) send_msg=msg_mock)
_help(bot=MagicBot(), update=update) _help(bot=MagicMock(), update=update)
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert '*/help:* `This help message`' in msg_mock.call_args_list[0][0][0] 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): def test_send_msg(default_conf, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch.multiple('freqtrade.main.telegram', mocker.patch.multiple('freqtrade.main.telegram',

51
scripts/plot_dataframe.py Executable file
View File

@ -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')