Merge branch 'release/0.14.1'
This commit is contained in:
commit
de13df6ede
@ -1,4 +1,5 @@
|
||||
[run]
|
||||
omit =
|
||||
scripts/*
|
||||
freqtrade/tests/*
|
||||
freqtrade/vendor/*
|
@ -1,3 +1,3 @@
|
||||
__version__ = '0.14.0'
|
||||
__version__ = '0.14.1'
|
||||
|
||||
from . import main
|
||||
|
@ -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)
|
||||
|
@ -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()))
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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 <trade_id>`')
|
||||
# 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 = '<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)
|
||||
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 <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`
|
||||
*/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
|
||||
|
34
freqtrade/tests/test_exchange.py
Normal file
34
freqtrade/tests/test_exchange.py
Normal 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'])
|
||||
|
@ -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)
|
||||
|
@ -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('</?pre>', '', 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 '<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(),
|
||||
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 = '<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):
|
||||
@ -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',
|
||||
|
51
scripts/plot_dataframe.py
Executable file
51
scripts/plot_dataframe.py
Executable 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')
|
||||
|
Loading…
Reference in New Issue
Block a user