Merge pull request #356 from kryofly/test_coverage

Test coverage
This commit is contained in:
Janne Sinivirta 2018-01-25 09:31:06 +02:00 committed by GitHub
commit 7dc63c06e7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 345 additions and 80 deletions

View File

@ -82,35 +82,39 @@ def analyze_ticker(ticker_history: List[Dict]) -> DataFrame:
return dataframe return dataframe
# FIX: Maybe return False, if an error has occured,
# Otherwise we might mask an error as an non-signal-scenario
def get_signal(pair: str, interval: int) -> (bool, bool): def get_signal(pair: str, interval: int) -> (bool, bool):
""" """
Calculates current signal based several technical analysis indicators Calculates current signal based several technical analysis indicators
:param pair: pair in format BTC_ANT or BTC-ANT :param pair: pair in format BTC_ANT or BTC-ANT
:return: (True, False) if pair is good for buying and not for selling :return: (Buy, Sell) A bool-tuple indicating buy/sell signal
""" """
ticker_hist = get_ticker_history(pair, interval) ticker_hist = get_ticker_history(pair, interval)
if not ticker_hist: if not ticker_hist:
logger.warning('Empty ticker history for pair %s', pair) logger.warning('Empty ticker history for pair %s', pair)
return (False, False) return (False, False) # return False ?
try: try:
dataframe = analyze_ticker(ticker_hist) dataframe = analyze_ticker(ticker_hist)
except ValueError as ex: except ValueError as ex:
logger.warning('Unable to analyze ticker for pair %s: %s', pair, str(ex)) logger.warning('Unable to analyze ticker for pair %s: %s', pair, str(ex))
return (False, False) return (False, False) # return False ?
except Exception as ex: except Exception as ex:
logger.exception('Unexpected error when analyzing ticker for pair %s: %s', pair, str(ex)) logger.exception('Unexpected error when analyzing ticker for pair %s: %s', pair, str(ex))
return (False, False) return (False, False) # return False ?
if dataframe.empty: if dataframe.empty:
return (False, False) logger.warning('Empty dataframe for pair %s', pair)
return (False, False) # return False ?
latest = dataframe.iloc[-1] latest = dataframe.iloc[-1]
# Check if dataframe is out of date # Check if dataframe is out of date
signal_date = arrow.get(latest['date']) signal_date = arrow.get(latest['date'])
if signal_date < arrow.now() - timedelta(minutes=10): if signal_date < arrow.now() - timedelta(minutes=10):
return (False, False) logger.warning('Too old dataframe for pair %s', pair)
return (False, False) # return False ?
(buy, sell) = latest[SignalType.BUY.value] == 1, latest[SignalType.SELL.value] == 1 (buy, sell) = latest[SignalType.BUY.value] == 1, latest[SignalType.SELL.value] == 1
logger.debug('trigger: %s (pair=%s) buy=%s sell=%s', latest['date'], pair, str(buy), str(sell)) logger.debug('trigger: %s (pair=%s) buy=%s sell=%s', latest['date'], pair, str(buy), str(sell))

View File

@ -55,12 +55,49 @@ def refresh_whitelist(whitelist: List[str]) -> List[str]:
return final_list return final_list
def process_maybe_execute_buy(conf, interval):
"""
Tries to execute a buy trade in a safe way
:return: True if executed
"""
try:
# Create entity and execute trade
if create_trade(float(_CONF['stake_amount']), interval):
return True
else:
logger.info(
'Checked all whitelisted currencies. '
'Found no suitable entry positions for buying. Will keep looking ...'
)
return False
except DependencyException as exception:
logger.warning('Unable to create trade: %s', exception)
return False
def process_maybe_execute_sell(trade, interval):
"""
Tries to execute a sell trade
:return: True if executed
"""
# Get order details for actual price per unit
if trade.open_order_id:
# Update trade with order values
logger.info('Got open order for %s', trade)
trade.update(exchange.get_order(trade.open_order_id))
if trade.is_open and trade.open_order_id is None:
# Check if we can sell our current pair
return handle_trade(trade, interval)
return False
def _process(interval: int, nb_assets: Optional[int] = 0) -> bool: def _process(interval: int, nb_assets: Optional[int] = 0) -> bool:
""" """
Queries the persistence layer for open trades and handles them, Queries the persistence layer for open trades and handles them,
otherwise a new trade is created. otherwise a new trade is created.
:param: nb_assets: the maximum number of pairs to be traded at the same time :param: nb_assets: the maximum number of pairs to be traded at the same time
:return: True if a trade has been created or closed, False otherwise :return: True if one or more trades has been created or closed, False otherwise
""" """
state_changed = False state_changed = False
try: try:
@ -78,33 +115,16 @@ def _process(interval: int, nb_assets: Optional[int] = 0) -> bool:
# Query trades from persistence layer # Query trades from persistence layer
trades = Trade.query.filter(Trade.is_open.is_(True)).all() trades = Trade.query.filter(Trade.is_open.is_(True)).all()
if len(trades) < _CONF['max_open_trades']: if len(trades) < _CONF['max_open_trades']:
try: state_changed = process_maybe_execute_buy(_CONF, interval)
# Create entity and execute trade
state_changed = create_trade(float(_CONF['stake_amount']), interval)
if not state_changed:
logger.info(
'Checked all whitelisted currencies. '
'Found no suitable entry positions for buying. Will keep looking ...'
)
except DependencyException as exception:
logger.warning('Unable to create trade: %s', exception)
for trade in trades: for trade in trades:
# Get order details for actual price per unit state_changed |= process_maybe_execute_sell(trade, interval)
if trade.open_order_id:
# Update trade with order values
logger.info('Got open order for %s', trade)
trade.update(exchange.get_order(trade.open_order_id))
if trade.is_open and trade.open_order_id is None:
# Check if we can sell our current pair
state_changed = handle_trade(trade, interval) or state_changed
if 'unfilledtimeout' in _CONF: if 'unfilledtimeout' in _CONF:
# Check and handle any timed out open orders # Check and handle any timed out open orders
check_handle_timedout(_CONF['unfilledtimeout']) check_handle_timedout(_CONF['unfilledtimeout'])
Trade.session.flush() Trade.session.flush()
except (requests.exceptions.RequestException, json.JSONDecodeError) as error: except (requests.exceptions.RequestException, json.JSONDecodeError) as error:
logger.warning( logger.warning(
'Got %s in _process(), retrying in 30 seconds...', 'Got %s in _process(), retrying in 30 seconds...',
@ -121,6 +141,59 @@ def _process(interval: int, nb_assets: Optional[int] = 0) -> bool:
return state_changed return state_changed
# FIX: 20180110, why is cancel.order unconditionally here, whereas
# it is conditionally called in the
# handle_timedout_limit_sell()?
def handle_timedout_limit_buy(trade: Trade, order: Dict) -> bool:
"""Buy timeout - cancel order
:return: True if order was fully cancelled
"""
exchange.cancel_order(trade.open_order_id)
if order['remaining'] == order['amount']:
# if trade is not partially completed, just delete the trade
Trade.session.delete(trade)
# FIX? do we really need to flush, caller of
# check_handle_timedout will flush afterwards
Trade.session.flush()
logger.info('Buy order timeout for %s.', trade)
rpc.send_msg('*Timeout:* Unfilled buy order for {} cancelled'.format(
trade.pair.replace('_', '/')))
return True
else:
# if trade is partially complete, edit the stake details for the trade
# and close the order
trade.amount = order['amount'] - order['remaining']
trade.stake_amount = trade.amount * trade.open_rate
trade.open_order_id = None
logger.info('Partial buy order timeout for %s.', trade)
rpc.send_msg('*Timeout:* Remaining buy order for {} cancelled'.format(
trade.pair.replace('_', '/')))
return False
# FIX: 20180110, should cancel_order() be cond. or unconditionally called?
def handle_timedout_limit_sell(trade: Trade, order: Dict) -> bool:
"""
Sell timeout - cancel order and update trade
:return: True if order was fully cancelled
"""
if order['remaining'] == order['amount']:
# if trade is not partially completed, just cancel the trade
exchange.cancel_order(trade.open_order_id)
trade.close_rate = None
trade.close_profit = None
trade.close_date = None
trade.is_open = True
trade.open_order_id = None
rpc.send_msg('*Timeout:* Unfilled sell order for {} cancelled'.format(
trade.pair.replace('_', '/')))
logger.info('Sell order timeout for %s.', trade)
return True
else:
# TODO: figure out how to handle partially complete sell orders
return False
def check_handle_timedout(timeoutvalue: int) -> None: def check_handle_timedout(timeoutvalue: int) -> None:
""" """
Check if any orders are timed out and cancel if neccessary Check if any orders are timed out and cancel if neccessary
@ -142,41 +215,9 @@ def check_handle_timedout(timeoutvalue: int) -> None:
continue continue
if order['type'] == "LIMIT_BUY" and ordertime < timeoutthreashold: if order['type'] == "LIMIT_BUY" and ordertime < timeoutthreashold:
# Buy timeout - cancel order handle_timedout_limit_buy(trade, order)
exchange.cancel_order(trade.open_order_id)
if order['remaining'] == order['amount']:
# if trade is not partially completed, just delete the trade
Trade.session.delete(trade)
Trade.session.flush()
logger.info('Buy order timeout for %s.', trade)
rpc.send_msg('*Timeout:* Unfilled buy order for {} cancelled'.format(
trade.pair.replace('_', '/')))
else:
# if trade is partially complete, edit the stake details for the trade
# and close the order
trade.amount = order['amount'] - order['remaining']
trade.stake_amount = trade.amount * trade.open_rate
trade.open_order_id = None
logger.info('Partial buy order timeout for %s.', trade)
rpc.send_msg('*Timeout:* Remaining buy order for {} cancelled'.format(
trade.pair.replace('_', '/')))
elif order['type'] == "LIMIT_SELL" and ordertime < timeoutthreashold: elif order['type'] == "LIMIT_SELL" and ordertime < timeoutthreashold:
# Sell timeout - cancel order and update trade handle_timedout_limit_sell(trade, order)
if order['remaining'] == order['amount']:
# if trade is not partially completed, just cancel the trade
exchange.cancel_order(trade.open_order_id)
trade.close_rate = None
trade.close_profit = None
trade.close_date = None
trade.is_open = True
trade.open_order_id = None
rpc.send_msg('*Timeout:* Unfilled sell order for {} cancelled'.format(
trade.pair.replace('_', '/')))
logger.info('Sell order timeout for %s.', trade)
return True
else:
# TODO: figure out how to handle partially complete sell orders
pass
def execute_sell(trade: Trade, limit: float) -> None: def execute_sell(trade: Trade, limit: float) -> None:
@ -435,7 +476,7 @@ def cleanup() -> None:
exit(0) exit(0)
def main(sysargv=sys.argv[1:]) -> None: def main(sysargv=sys.argv[1:]) -> int:
""" """
Loads and validates the config and handles the main loop Loads and validates the config and handles the main loop
:return: None :return: None
@ -447,7 +488,7 @@ def main(sysargv=sys.argv[1:]) -> None:
# A subcommand has been issued # A subcommand has been issued
if hasattr(args, 'func'): if hasattr(args, 'func'):
args.func(args) args.func(args)
exit(0) return 0
# Initialize logger # Initialize logger
logging.basicConfig( logging.basicConfig(
@ -508,7 +549,8 @@ def main(sysargv=sys.argv[1:]) -> None:
logger.exception('Got fatal exception!') logger.exception('Got fatal exception!')
finally: finally:
cleanup() cleanup()
return 0
if __name__ == '__main__': if __name__ == '__main__':
main() main(sys.argv[1:])

View File

@ -109,6 +109,12 @@ def download_pairs(datadir, pairs: List[str], ticker_interval: int) -> bool:
return True return True
def file_dump_json(filename, data):
with open(filename, "wt") as fp:
json.dump(data, fp)
# FIX: 20180110, suggest rename interval to tick_interval
def download_backtesting_testdata(datadir: str, pair: str, interval: int = 5) -> bool: def download_backtesting_testdata(datadir: str, pair: str, interval: int = 5) -> bool:
""" """
Download the latest 1 and 5 ticker intervals from Bittrex for the pairs passed in parameters Download the latest 1 and 5 ticker intervals from Bittrex for the pairs passed in parameters

View File

@ -1,6 +1,7 @@
# pragma pylint: disable=missing-docstring # pragma pylint: disable=missing-docstring
from datetime import datetime from datetime import datetime
from unittest.mock import MagicMock from unittest.mock import MagicMock
from functools import reduce
import arrow import arrow
import pytest import pytest
@ -10,6 +11,14 @@ from telegram import Chat, Message, Update
from freqtrade.misc import CONF_SCHEMA from freqtrade.misc import CONF_SCHEMA
def log_has(line, logs):
# caplog mocker returns log as a tuple: ('freqtrade.analyze', logging.WARNING, 'foobar')
# and we want to match line against foobar in the tuple
return reduce(lambda a, b: a or b,
filter(lambda x: x[2] == line, logs),
False)
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def default_conf(): def default_conf():
""" Returns validated configuration suitable for most tests """ """ Returns validated configuration suitable for most tests """

View File

@ -7,13 +7,23 @@ import pytest
from freqtrade import OperationalException from freqtrade import OperationalException
from freqtrade.exchange import init, validate_pairs, buy, sell, get_balance, get_balances, \ from freqtrade.exchange import init, validate_pairs, buy, sell, get_balance, get_balances, \
get_ticker, cancel_order, get_name, get_fee get_ticker, get_ticker_history, cancel_order, get_name, get_fee
import freqtrade.exchange as exchange
API_INIT = False
def maybe_init_api(conf, mocker):
global API_INIT
if not API_INIT:
mocker.patch('freqtrade.exchange.validate_pairs',
side_effect=lambda s: True)
init(config=conf)
API_INIT = True
def test_init(default_conf, mocker, caplog): def test_init(default_conf, mocker, caplog):
mocker.patch('freqtrade.exchange.validate_pairs', maybe_init_api(default_conf, mocker)
side_effect=lambda s: True)
init(config=default_conf)
assert ('freqtrade.exchange', assert ('freqtrade.exchange',
logging.INFO, logging.INFO,
'Instance is running with dry_run enabled' 'Instance is running with dry_run enabled'
@ -159,8 +169,10 @@ def test_get_balances_prod(default_conf, mocker):
assert get_balances()[0]['Pending'] == 0.0 assert get_balances()[0]['Pending'] == 0.0
def test_get_ticker(mocker, ticker): # This test is somewhat redundant with
# test_exchange_bittrex.py::test_exchange_bittrex_get_ticker
def test_get_ticker(default_conf, mocker, ticker):
maybe_init_api(default_conf, mocker)
api_mock = MagicMock() api_mock = MagicMock()
tick = {"success": True, 'result': {'Bid': 0.00001098, 'Ask': 0.00001099, 'Last': 0.0001}} tick = {"success": True, 'result': {'Bid': 0.00001098, 'Ask': 0.00001099, 'Last': 0.0001}}
api_mock.get_ticker = MagicMock(return_value=tick) api_mock.get_ticker = MagicMock(return_value=tick)
@ -177,6 +189,7 @@ def test_get_ticker(mocker, ticker):
mocker.patch('freqtrade.exchange.bittrex._API', api_mock) mocker.patch('freqtrade.exchange.bittrex._API', api_mock)
# if not caching the result we should get the same ticker # if not caching the result we should get the same ticker
# if not fetching a new result we should get the cached ticker
ticker = get_ticker(pair='BTC_ETH', refresh=False) ticker = get_ticker(pair='BTC_ETH', refresh=False)
assert ticker['bid'] == 0.00001098 assert ticker['bid'] == 0.00001098
assert ticker['ask'] == 0.00001099 assert ticker['ask'] == 0.00001099
@ -187,6 +200,26 @@ def test_get_ticker(mocker, ticker):
assert ticker['ask'] == 1 assert ticker['ask'] == 1
def test_get_ticker_history(default_conf, mocker, ticker):
api_mock = MagicMock()
tick = 123
api_mock.get_ticker_history = MagicMock(return_value=tick)
mocker.patch('freqtrade.exchange._API', api_mock)
# retrieve original ticker
ticks = get_ticker_history('BTC_ETH', int(default_conf['ticker_interval']))
assert ticks == 123
# change the ticker
tick = 999
api_mock.get_ticker_history = MagicMock(return_value=tick)
mocker.patch('freqtrade.exchange._API', api_mock)
# ensure caching will still return the original ticker
ticks = get_ticker_history('BTC_ETH', int(default_conf['ticker_interval']))
assert ticks == 123
def test_cancel_order_dry_run(default_conf, mocker): def test_cancel_order_dry_run(default_conf, mocker):
default_conf['dry_run'] = True default_conf['dry_run'] = True
mocker.patch.dict('freqtrade.exchange._CONF', default_conf) mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
@ -194,6 +227,33 @@ def test_cancel_order_dry_run(default_conf, mocker):
assert cancel_order(order_id='123') is None assert cancel_order(order_id='123') is None
# Ensure that if not dry_run, we should call API
def test_cancel_order(default_conf, mocker):
default_conf['dry_run'] = False
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
api_mock = MagicMock()
api_mock.cancel_order = MagicMock(return_value=123)
mocker.patch('freqtrade.exchange._API', api_mock)
assert cancel_order(order_id='_') == 123
def test_get_order(default_conf, mocker):
default_conf['dry_run'] = True
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
order = MagicMock()
order.myid = 123
exchange._DRY_RUN_OPEN_ORDERS['X'] = order
print(exchange.get_order('X'))
assert exchange.get_order('X').myid == 123
default_conf['dry_run'] = False
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
api_mock = MagicMock()
api_mock.get_order = MagicMock(return_value=456)
mocker.patch('freqtrade.exchange._API', api_mock)
assert 456 == exchange.get_order('X')
def test_get_name(default_conf, mocker): def test_get_name(default_conf, mocker):
mocker.patch('freqtrade.exchange.validate_pairs', mocker.patch('freqtrade.exchange.validate_pairs',
side_effect=lambda s: True) side_effect=lambda s: True)
@ -209,3 +269,18 @@ def test_get_fee(default_conf, mocker):
init(default_conf) init(default_conf)
assert get_fee() == 0.0025 assert get_fee() == 0.0025
def test_exchange_misc(default_conf, mocker):
api_mock = MagicMock()
mocker.patch('freqtrade.exchange._API', api_mock)
exchange.get_markets()
assert 1 == api_mock.get_markets.call_count
exchange.get_market_summaries()
assert 1 == api_mock.get_market_summaries.call_count
api_mock.name = 123
assert 123 == exchange.get_name()
api_mock.fee = 456
assert 456 == exchange.get_fee()
exchange.get_wallet_health()
assert 1 == api_mock.get_wallet_health.call_count

View File

@ -143,7 +143,7 @@ def test_exchange_bittrex_fee():
assert fee >= 0 and fee < 0.1 # Fee is 0-10 % assert fee >= 0 and fee < 0.1 # Fee is 0-10 %
def test_exchange_bittrex_buy_good(mocker): def test_exchange_bittrex_buy_good():
wb = make_wrap_bittrex() wb = make_wrap_bittrex()
fb = FakeBittrex() fb = FakeBittrex()
uuid = wb.buy('BTC_ETH', 1, 1) uuid = wb.buy('BTC_ETH', 1, 1)
@ -154,7 +154,7 @@ def test_exchange_bittrex_buy_good(mocker):
wb.buy('BAD', 1, 1) wb.buy('BAD', 1, 1)
def test_exchange_bittrex_sell_good(mocker): def test_exchange_bittrex_sell_good():
wb = make_wrap_bittrex() wb = make_wrap_bittrex()
fb = FakeBittrex() fb = FakeBittrex()
uuid = wb.sell('BTC_ETH', 1, 1) uuid = wb.sell('BTC_ETH', 1, 1)
@ -165,7 +165,7 @@ def test_exchange_bittrex_sell_good(mocker):
uuid = wb.sell('BAD', 1, 1) uuid = wb.sell('BAD', 1, 1)
def test_exchange_bittrex_get_balance(mocker): def test_exchange_bittrex_get_balance():
wb = make_wrap_bittrex() wb = make_wrap_bittrex()
fb = FakeBittrex() fb = FakeBittrex()
bal = wb.get_balance('BTC_ETH') bal = wb.get_balance('BTC_ETH')
@ -238,10 +238,12 @@ def test_exchange_bittrex_get_ticker_bad():
wb.get_ticker('BTC_ETH') wb.get_ticker('BTC_ETH')
def test_exchange_bittrex_get_ticker_history_one(): def test_exchange_bittrex_get_ticker_history_intervals():
wb = make_wrap_bittrex() wb = make_wrap_bittrex()
FakeBittrex() FakeBittrex()
assert wb.get_ticker_history('BTC_ETH', 1) for tick_interval in [1, 5, 30, 60, 1440]:
assert ([{'C': 0, 'V': 0, 'O': 0, 'H': 0, 'L': 0, 'T': 0}] ==
wb.get_ticker_history('BTC_ETH', tick_interval))
def test_exchange_bittrex_get_ticker_history(): def test_exchange_bittrex_get_ticker_history():
@ -351,3 +353,8 @@ def test_validate_response_min_trade_requirement_not_met():
} }
with pytest.raises(ContentDecodingError, match=r'.*MIN_TRADE_REQUIREMENT_NOT_MET.*'): with pytest.raises(ContentDecodingError, match=r'.*MIN_TRADE_REQUIREMENT_NOT_MET.*'):
Bittrex._validate_response(response) Bittrex._validate_response(response)
def test_custom_requests(mocker):
mocker.patch('freqtrade.exchange.bittrex.requests', MagicMock())
btx.custom_requests('http://', '')

View File

@ -2,6 +2,7 @@
import os import os
import logging import logging
# from unittest.mock import MagicMock
from shutil import copyfile from shutil import copyfile
from freqtrade import exchange, optimize from freqtrade import exchange, optimize
from freqtrade.exchange import Bittrex from freqtrade.exchange import Bittrex
@ -198,12 +199,27 @@ def test_download_backtesting_testdata(default_conf, ticker_history, mocker):
_clean_test_file(file2) _clean_test_file(file2)
def test_download_backtesting_testdata2(default_conf, mocker):
tick = [{'T': 'bar'}, {'T': 'foo'}]
mocker.patch('freqtrade.misc.file_dump_json', return_value=None)
mocker.patch('freqtrade.optimize.__init__.get_ticker_history', return_value=tick)
assert download_backtesting_testdata(None, pair="BTC-UNITEST", interval=1)
assert download_backtesting_testdata(None, pair="BTC-UNITEST", interval=3)
def test_load_tickerdata_file(): def test_load_tickerdata_file():
assert not load_tickerdata_file(None, 'BTC_UNITEST', 7) assert not load_tickerdata_file(None, 'BTC_UNITEST', 7)
tickerdata = load_tickerdata_file(None, 'BTC_UNITEST', 1) tickerdata = load_tickerdata_file(None, 'BTC_UNITEST', 1)
assert _btc_unittest_length == len(tickerdata) assert _btc_unittest_length == len(tickerdata)
def test_init(default_conf, mocker):
conf = {'exchange': {'pair_whitelist': []}}
mocker.patch('freqtrade.optimize.hyperopt_optimize_conf', return_value=conf)
assert {} == optimize.load_data('', pairs=[], refresh_pairs=True,
ticker_interval=int(default_conf['ticker_interval']))
def test_tickerdata_to_dataframe(): def test_tickerdata_to_dataframe():
timerange = ((None, 'line'), None, -100) timerange = ((None, 'line'), None, -100)
tick = load_tickerdata_file(None, 'BTC_UNITEST', 1, timerange=timerange) tick = load_tickerdata_file(None, 'BTC_UNITEST', 1, timerange=timerange)

View File

@ -1,8 +1,10 @@
# pragma pylint: disable=missing-docstring,W0621 # pragma pylint: disable=missing-docstring,W0621
import json import json
from unittest.mock import MagicMock from unittest.mock import MagicMock
import freqtrade.tests.conftest as tt # test tools
import arrow import arrow
import datetime
import pytest import pytest
from pandas import DataFrame from pandas import DataFrame
@ -73,6 +75,41 @@ def test_returns_latest_sell_signal(mocker):
assert get_signal('BTC-ETH', 5) == (True, False) assert get_signal('BTC-ETH', 5) == (True, False)
def test_get_signal_empty(default_conf, mocker, caplog):
mocker.patch('freqtrade.analyze.get_ticker_history', return_value=None)
assert (False, False) == get_signal('foo', int(default_conf['ticker_interval']))
assert tt.log_has('Empty ticker history for pair foo',
caplog.record_tuples)
def test_get_signal_exception_valueerror(default_conf, mocker, caplog):
mocker.patch('freqtrade.analyze.get_ticker_history', return_value=1)
mocker.patch('freqtrade.analyze.analyze_ticker',
side_effect=ValueError('xyz'))
assert (False, False) == get_signal('foo', int(default_conf['ticker_interval']))
assert tt.log_has('Unable to analyze ticker for pair foo: xyz',
caplog.record_tuples)
def test_get_signal_empty_dataframe(default_conf, mocker, caplog):
mocker.patch('freqtrade.analyze.get_ticker_history', return_value=1)
mocker.patch('freqtrade.analyze.analyze_ticker', return_value=DataFrame([]))
assert (False, False) == get_signal('xyz', int(default_conf['ticker_interval']))
assert tt.log_has('Empty dataframe for pair xyz',
caplog.record_tuples)
def test_get_signal_old_dataframe(default_conf, mocker, caplog):
mocker.patch('freqtrade.analyze.get_ticker_history', return_value=1)
# FIX: The get_signal function has hardcoded 10, which we must inturn hardcode
oldtime = arrow.utcnow() - datetime.timedelta(minutes=11)
ticks = DataFrame([{'buy': 1, 'date': oldtime}])
mocker.patch('freqtrade.analyze.analyze_ticker', return_value=DataFrame(ticks))
assert (False, False) == get_signal('xyz', int(default_conf['ticker_interval']))
assert tt.log_has('Too old dataframe for pair xyz',
caplog.record_tuples)
def test_get_signal_handles_exceptions(mocker): def test_get_signal_handles_exceptions(mocker):
mocker.patch('freqtrade.analyze.get_ticker_history', return_value=MagicMock()) mocker.patch('freqtrade.analyze.get_ticker_history', return_value=MagicMock())
mocker.patch('freqtrade.analyze.analyze_ticker', mocker.patch('freqtrade.analyze.analyze_ticker',

View File

@ -2,6 +2,7 @@
import copy import copy
import logging import logging
from unittest.mock import MagicMock from unittest.mock import MagicMock
import freqtrade.tests.conftest as tt # test tools
import arrow import arrow
import pytest import pytest
@ -20,11 +21,10 @@ from freqtrade.persistence import Trade
def test_parse_args_backtesting(mocker): def test_parse_args_backtesting(mocker):
""" Test that main() can start backtesting or hyperopt. """ Test that main() can start backtesting or hyperopt.
and also ensure we can pass some specific arguments and also ensure we can pass some specific arguments
argument parsing is done in test_misc.py """ further argument parsing is done in test_misc.py """
backtesting_mock = mocker.patch( backtesting_mock = mocker.patch(
'freqtrade.optimize.backtesting.start', MagicMock()) 'freqtrade.optimize.backtesting.start', MagicMock())
with pytest.raises(SystemExit, match=r'0'): main.main(['backtesting'])
main.main(['backtesting'])
assert backtesting_mock.call_count == 1 assert backtesting_mock.call_count == 1
call_args = backtesting_mock.call_args[0][0] call_args = backtesting_mock.call_args[0][0]
assert call_args.config == 'config.json' assert call_args.config == 'config.json'
@ -38,8 +38,7 @@ def test_parse_args_backtesting(mocker):
def test_main_start_hyperopt(mocker): def test_main_start_hyperopt(mocker):
hyperopt_mock = mocker.patch( hyperopt_mock = mocker.patch(
'freqtrade.optimize.hyperopt.start', MagicMock()) 'freqtrade.optimize.hyperopt.start', MagicMock())
with pytest.raises(SystemExit, match=r'0'): main.main(['hyperopt'])
main.main(['hyperopt'])
assert hyperopt_mock.call_count == 1 assert hyperopt_mock.call_count == 1
call_args = hyperopt_mock.call_args[0][0] call_args = hyperopt_mock.call_args[0][0]
assert call_args.config == 'config.json' assert call_args.config == 'config.json'
@ -48,6 +47,34 @@ def test_main_start_hyperopt(mocker):
assert call_args.func is not None assert call_args.func is not None
def test_process_maybe_execute_buy(default_conf, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.create_trade', return_value=True)
assert main.process_maybe_execute_buy(default_conf, int(default_conf['ticker_interval']))
mocker.patch('freqtrade.main.create_trade', return_value=False)
assert not main.process_maybe_execute_buy(default_conf, int(default_conf['ticker_interval']))
def test_process_maybe_execute_sell(default_conf, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.handle_trade', return_value=True)
mocker.patch('freqtrade.exchange.get_order', return_value=1)
trade = MagicMock()
trade.open_order_id = '123'
assert not main.process_maybe_execute_sell(trade, int(default_conf['ticker_interval']))
trade.is_open = True
trade.open_order_id = None
# Assert we call handle_trade() if trade is feasible for execution
assert main.process_maybe_execute_sell(trade, int(default_conf['ticker_interval']))
def test_process_maybe_execute_buy_exception(default_conf, mocker, caplog):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.create_trade', MagicMock(side_effect=DependencyException))
main.process_maybe_execute_buy(default_conf, int(default_conf['ticker_interval']))
tt.log_has('Unable to create trade:', caplog.record_tuples)
def test_process_trade_creation(default_conf, ticker, limit_buy_order, health, mocker): def test_process_trade_creation(default_conf, ticker, limit_buy_order, health, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock()) mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
@ -229,6 +256,20 @@ def test_create_trade_no_pairs_after_blacklist(default_conf, ticker, mocker):
create_trade(default_conf['stake_amount'], int(default_conf['ticker_interval'])) create_trade(default_conf['stake_amount'], int(default_conf['ticker_interval']))
def test_create_trade_no_signal(default_conf, ticker, mocker):
default_conf['dry_run'] = True
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_signal', MagicMock(return_value=(False, False)))
mocker.patch.multiple('freqtrade.exchange',
get_ticker_history=MagicMock(return_value=20))
mocker.patch.multiple('freqtrade.main.exchange',
get_balance=MagicMock(return_value=20))
stake_amount = 10
Trade.query = MagicMock()
Trade.query.filter = MagicMock()
assert not create_trade(stake_amount, int(default_conf['ticker_interval']))
def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, mocker): def test_handle_trade(default_conf, 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_signal', side_effect=lambda s, t: (True, False)) mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
@ -430,6 +471,20 @@ def test_check_handle_timedout_buy(default_conf, ticker, limit_buy_order_old, mo
assert len(trades) == 0 assert len(trades) == 0
def test_handle_timedout_limit_buy(default_conf, mocker):
cancel_order = MagicMock()
mocker.patch('freqtrade.exchange.cancel_order', cancel_order)
Trade.session = MagicMock()
trade = MagicMock()
order = {'remaining': 1,
'amount': 1}
assert main.handle_timedout_limit_buy(trade, order)
assert cancel_order.call_count == 1
order['amount'] = 2
assert not main.handle_timedout_limit_buy(trade, order)
assert cancel_order.call_count == 2
def test_check_handle_timedout_sell(default_conf, ticker, limit_sell_order_old, mocker): def test_check_handle_timedout_sell(default_conf, ticker, limit_sell_order_old, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch.dict('freqtrade.main._CONF', default_conf)
cancel_order_mock = MagicMock() cancel_order_mock = MagicMock()
@ -464,6 +519,20 @@ def test_check_handle_timedout_sell(default_conf, ticker, limit_sell_order_old,
assert trade_sell.is_open is True assert trade_sell.is_open is True
def test_handle_timedout_limit_sell(default_conf, mocker):
cancel_order = MagicMock()
mocker.patch('freqtrade.exchange.cancel_order', cancel_order)
trade = MagicMock()
order = {'remaining': 1,
'amount': 1}
assert main.handle_timedout_limit_sell(trade, order)
assert cancel_order.call_count == 1
order['amount'] = 2
assert not main.handle_timedout_limit_sell(trade, order)
# Assert cancel_order was not called (callcount remains unchanged)
assert cancel_order.call_count == 1
def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old_partial, def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old_partial,
mocker): mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch.dict('freqtrade.main._CONF', default_conf)