From e81a9cbb17d33fff15381638e5040a8562a9bb2c Mon Sep 17 00:00:00 2001 From: Gerald Lonlas Date: Fri, 29 Dec 2017 23:12:52 -0800 Subject: [PATCH 1/2] Increase code coverage Change log: * Increase code coverage for test_exchange.py * Move Exchange Unit tests files tests/exchange/ * Move RPC Unit tests files tests/rpc/ --- freqtrade/tests/exchange/test_exchange.py | 188 ++++++++++++++++++ .../{ => exchange}/test_exchange_bittrex.py | 0 freqtrade/tests/{ => rpc}/test_rpc.py | 0 .../tests/{ => rpc}/test_rpc_telegram.py | 0 freqtrade/tests/test_exchange.py | 36 ---- 5 files changed, 188 insertions(+), 36 deletions(-) create mode 100644 freqtrade/tests/exchange/test_exchange.py rename freqtrade/tests/{ => exchange}/test_exchange_bittrex.py (100%) rename freqtrade/tests/{ => rpc}/test_rpc.py (100%) rename freqtrade/tests/{ => rpc}/test_rpc_telegram.py (100%) delete mode 100644 freqtrade/tests/test_exchange.py diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py new file mode 100644 index 000000000..2da657642 --- /dev/null +++ b/freqtrade/tests/exchange/test_exchange.py @@ -0,0 +1,188 @@ +# pragma pylint: disable=missing-docstring,C0103 +from unittest.mock import MagicMock +from requests.exceptions import RequestException +from random import randint +import logging +import pytest + +from freqtrade import OperationalException +from freqtrade.exchange import init, validate_pairs, buy, sell, get_balance, get_balances, \ + get_ticker, cancel_order, get_name, get_fee + + +def test_init(default_conf, mocker, caplog): + mocker.patch('freqtrade.exchange.validate_pairs', side_effect=lambda s: True) + init(config=default_conf) + assert ('freqtrade.exchange', + logging.INFO, + 'Instance is running with dry_run enabled' + ) in caplog.record_tuples + + +def test_init_exception(default_conf, mocker): + default_conf['exchange']['name'] = 'wrong_exchange_name' + + with pytest.raises( + OperationalException, + match='Exchange {} is not supported'.format(default_conf['exchange']['name'])): + init(config=default_conf) + + +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', 'BTC_BCC', + ]) + 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(OperationalException, 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(OperationalException, match=r'not compatible'): + validate_pairs(default_conf['exchange']['pair_whitelist']) + + +def test_validate_pairs_exception(default_conf, mocker, caplog): + api_mock = MagicMock() + api_mock.get_markets = MagicMock(side_effect=RequestException()) + mocker.patch('freqtrade.exchange._API', api_mock) + + # with pytest.raises(RequestException, match=r'Unable to validate pairs'): + validate_pairs(default_conf['exchange']['pair_whitelist']) + assert ('freqtrade.exchange', + logging.WARNING, + 'Unable to validate pairs (assuming they are correct). Reason: ' + ) in caplog.record_tuples + + +def test_buy_dry_run(default_conf, mocker): + default_conf['dry_run'] = True + mocker.patch.dict('freqtrade.exchange._CONF', default_conf) + + assert 'dry_run_buy_' in buy(pair='BTC_ETH', rate=200, amount=1) + + +def test_buy_prod(default_conf, mocker): + api_mock = MagicMock() + api_mock.buy = MagicMock(return_value='dry_run_buy_{}'.format(randint(0, 10**6))) + mocker.patch('freqtrade.exchange._API', api_mock) + + default_conf['dry_run'] = False + mocker.patch.dict('freqtrade.exchange._CONF', default_conf) + + assert 'dry_run_buy_' in buy(pair='BTC_ETH', rate=200, amount=1) + + +def test_sell_dry_run(default_conf, mocker): + default_conf['dry_run'] = True + mocker.patch.dict('freqtrade.exchange._CONF', default_conf) + + assert 'dry_run_sell_' in sell(pair='BTC_ETH', rate=200, amount=1) + + +def test_sell_prod(default_conf, mocker): + api_mock = MagicMock() + api_mock.sell = MagicMock(return_value='dry_run_sell_{}'.format(randint(0, 10**6))) + mocker.patch('freqtrade.exchange._API', api_mock) + + default_conf['dry_run'] = False + mocker.patch.dict('freqtrade.exchange._CONF', default_conf) + + assert 'dry_run_sell_' in sell(pair='BTC_ETH', rate=200, amount=1) + + +def test_get_balance_dry_run(default_conf, mocker): + default_conf['dry_run'] = True + mocker.patch.dict('freqtrade.exchange._CONF', default_conf) + + assert get_balance(currency='BTC') == 999.9 + + +def test_get_balance_prod(default_conf, mocker): + api_mock = MagicMock() + api_mock.get_balance = MagicMock(return_value=123.4) + mocker.patch('freqtrade.exchange._API', api_mock) + + default_conf['dry_run'] = False + mocker.patch.dict('freqtrade.exchange._CONF', default_conf) + + assert get_balance(currency='BTC') == 123.4 + + +def test_get_balances_dry_run(default_conf, mocker): + default_conf['dry_run'] = True + mocker.patch.dict('freqtrade.exchange._CONF', default_conf) + + assert get_balances() == [] + + +def test_get_balances_prod(default_conf, mocker): + balance_item = { + 'Currency': '1ST', + 'Balance': 10.0, + 'Available': 10.0, + 'Pending': 0.0, + 'CryptoAddress': None + } + + api_mock = MagicMock() + api_mock.get_balances = MagicMock(return_value=[balance_item, balance_item, balance_item]) + mocker.patch('freqtrade.exchange._API', api_mock) + + default_conf['dry_run'] = False + mocker.patch.dict('freqtrade.exchange._CONF', default_conf) + + assert len(get_balances()) == 3 + assert get_balances()[0]['Currency'] == '1ST' + assert get_balances()[0]['Balance'] == 10.0 + assert get_balances()[0]['Available'] == 10.0 + assert get_balances()[0]['Pending'] == 0.0 + + +def test_get_ticker(mocker, ticker): + + api_mock = MagicMock() + api_mock.get_ticker = MagicMock(return_value=ticker()) + mocker.patch('freqtrade.exchange._API', api_mock) + + ticker = get_ticker(pair='BTC_ETH') + assert ticker['bid'] == 0.00001098 + assert ticker['ask'] == 0.00001099 + assert ticker['bid'] == 0.00001098 + + +def test_cancel_order_dry_run(default_conf, mocker): + default_conf['dry_run'] = True + mocker.patch.dict('freqtrade.exchange._CONF', default_conf) + + assert cancel_order(order_id='123') is None + + +def test_get_name(default_conf, mocker): + mocker.patch('freqtrade.exchange.validate_pairs', side_effect=lambda s: True) + default_conf['exchange']['name'] = 'bittrex' + init(default_conf) + + assert get_name() == 'Bittrex' + + +def test_get_fee(default_conf, mocker): + mocker.patch('freqtrade.exchange.validate_pairs', side_effect=lambda s: True) + init(default_conf) + + assert get_fee() == 0.0025 diff --git a/freqtrade/tests/test_exchange_bittrex.py b/freqtrade/tests/exchange/test_exchange_bittrex.py similarity index 100% rename from freqtrade/tests/test_exchange_bittrex.py rename to freqtrade/tests/exchange/test_exchange_bittrex.py diff --git a/freqtrade/tests/test_rpc.py b/freqtrade/tests/rpc/test_rpc.py similarity index 100% rename from freqtrade/tests/test_rpc.py rename to freqtrade/tests/rpc/test_rpc.py diff --git a/freqtrade/tests/test_rpc_telegram.py b/freqtrade/tests/rpc/test_rpc_telegram.py similarity index 100% rename from freqtrade/tests/test_rpc_telegram.py rename to freqtrade/tests/rpc/test_rpc_telegram.py diff --git a/freqtrade/tests/test_exchange.py b/freqtrade/tests/test_exchange.py deleted file mode 100644 index 78bb89bfc..000000000 --- a/freqtrade/tests/test_exchange.py +++ /dev/null @@ -1,36 +0,0 @@ -# pragma pylint: disable=missing-docstring,C0103 -from unittest.mock import MagicMock - -import pytest - -from freqtrade import OperationalException -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', 'BTC_BCC', - ]) - 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(OperationalException, 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(OperationalException, match=r'not compatible'): - validate_pairs(default_conf['exchange']['pair_whitelist']) From f7398e615add117521655ef138d2a8fc79d718e9 Mon Sep 17 00:00:00 2001 From: kryofly <34599184+kryofly@users.noreply.github.com> Date: Sat, 30 Dec 2017 11:55:23 +0100 Subject: [PATCH 2/2] Improve backtesting tests (#256) * test bugfix dataframe trimming * flake8 (as usual) * tests backtesting cleanup and bugfix * flake8 * test backtesting::start() * tests cleanup set() usage * tests: add missing assert --- freqtrade/tests/optimize/test_backtesting.py | 131 ++++++++++--------- freqtrade/tests/test_acl_pair.py | 13 +- 2 files changed, 70 insertions(+), 74 deletions(-) diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index 0d153a22e..94c062cac 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -2,9 +2,11 @@ import math import pandas as pd +# from unittest.mock import MagicMock from freqtrade import exchange, optimize from freqtrade.exchange import Bittrex from freqtrade.optimize.backtesting import backtest, generate_text_table, get_timeframe +# import freqtrade.optimize.backtesting as backtesting def test_generate_text_table(): @@ -49,77 +51,53 @@ def test_backtest_1min_ticker_interval(default_conf, mocker): assert not results.empty -def trim_dataframe(df, num): - new = dict() - for pair, pair_data in df.items(): - new[pair] = pair_data[-num:] # last 50 rows +def trim_dictlist(dl, num): + new = {} + for pair, pair_data in dl.items(): + # Can't figure out why -num wont work + new[pair] = pair_data[num:] return new def load_data_test(what): data = optimize.load_data(ticker_interval=1, pairs=['BTC_UNITEST']) - data = trim_dataframe(data, -40) + data = trim_dictlist(data, -100) pair = data['BTC_UNITEST'] - + datalen = len(pair) # Depending on the what parameter we now adjust the - # loaded data: + # loaded data looks: # pair :: [{'O': 0.123, 'H': 0.123, 'L': 0.123, # 'C': 0.123, 'V': 123.123, # 'T': '2017-11-04T23:02:00', 'BV': 0.123}] + base = 0.001 if what == 'raise': - o = 0.001 - h = 0.001 - ll = 0.001 - c = 0.001 - ll -= 0.0001 - h += 0.0001 - for frame in pair: - o += 0.0001 - h += 0.0001 - ll += 0.0001 - c += 0.0001 - # save prices rounded to satoshis - frame['O'] = round(o, 9) - frame['H'] = round(h, 9) - frame['L'] = round(ll, 9) - frame['C'] = round(c, 9) + return {'BTC_UNITEST': + [{'T': pair[x]['T'], # Keep old dates + 'V': pair[x]['V'], # Keep old volume + 'BV': pair[x]['BV'], # keep too + 'O': x * base, # But replace O,H,L,C + 'H': x * base + 0.0001, + 'L': x * base - 0.0001, + 'C': x * base} for x in range(0, datalen)]} if what == 'lower': - o = 0.001 - h = 0.001 - ll = 0.001 - c = 0.001 - ll -= 0.0001 - h += 0.0001 - for frame in pair: - o -= 0.0001 - h -= 0.0001 - ll -= 0.0001 - c -= 0.0001 - # save prices rounded to satoshis - frame['O'] = round(o, 9) - frame['H'] = round(h, 9) - frame['L'] = round(ll, 9) - frame['C'] = round(c, 9) + return {'BTC_UNITEST': + [{'T': pair[x]['T'], # Keep old dates + 'V': pair[x]['V'], # Keep old volume + 'BV': pair[x]['BV'], # keep too + 'O': 1 - x * base, # But replace O,H,L,C + 'H': 1 - x * base + 0.0001, + 'L': 1 - x * base - 0.0001, + 'C': 1 - x * base} for x in range(0, datalen)]} if what == 'sine': - i = 0 - o = (2 + math.sin(i/10)) / 1000 - h = o - ll = o - c = o - h += 0.0001 - ll -= 0.0001 - for frame in pair: - o = (2 + math.sin(i/10)) / 1000 - h = (2 + math.sin(i/10)) / 1000 + 0.0001 - ll = (2 + math.sin(i/10)) / 1000 - 0.0001 - c = (2 + math.sin(i/10)) / 1000 - 0.000001 - - # save prices rounded to satoshis - frame['O'] = round(o, 9) - frame['H'] = round(h, 9) - frame['L'] = round(ll, 9) - frame['C'] = round(c, 9) - i += 1 + hz = 0.1 # frequency + return {'BTC_UNITEST': + [{'T': pair[x]['T'], # Keep old dates + 'V': pair[x]['V'], # Keep old volume + 'BV': pair[x]['BV'], # keep too + 'O': math.sin(x*hz) / 1000 + base, # But replace O,H,L,C + 'H': math.sin(x*hz) / 1000 + base + 0.0001, + 'L': math.sin(x*hz) / 1000 + base - 0.0001, + 'C': math.sin(x*hz) / 1000 + base} for x in range(0, datalen)]} return data @@ -131,6 +109,7 @@ def simple_backtest(config, contour, num_results): # results :: assert len(results) == num_results + # Test backtest on offline data # loaded by freqdata/optimize/__init__.py::load_data() @@ -139,18 +118,42 @@ def test_backtest2(default_conf, mocker): mocker.patch.dict('freqtrade.main._CONF', default_conf) data = optimize.load_data(ticker_interval=5, pairs=['BTC_ETH']) results = backtest(default_conf['stake_amount'], optimize.preprocess(data), 10, True) - num_resutls = len(results) - assert num_resutls > 0 + assert not results.empty def test_processed(default_conf, mocker): mocker.patch.dict('freqtrade.main._CONF', default_conf) - data = load_data_test('raise') - assert optimize.preprocess(data) + dict_of_tickerrows = load_data_test('raise') + dataframes = optimize.preprocess(dict_of_tickerrows) + dataframe = dataframes['BTC_UNITEST'] + cols = dataframe.columns + # assert the dataframe got some of the indicator columns + for col in ['close', 'high', 'low', 'open', 'date', + 'ema50', 'ao', 'macd', 'plus_dm']: + assert col in cols -def test_raise(default_conf, mocker): +def test_backtest_pricecontours(default_conf, mocker): mocker.patch.dict('freqtrade.main._CONF', default_conf) - tests = [['raise', 359], ['lower', 0], ['sine', 1734]] + tests = [['raise', 17], ['lower', 0], ['sine', 17]] for [contour, numres] in tests: simple_backtest(default_conf, contour, numres) + +# Please make this work, the load_config needs to be mocked +# and cleanups. +# def test_backtest_start(default_conf, mocker): +# default_conf['exchange']['pair_whitelist'] = ['BTC_UNITEST'] +# mocker.patch.dict('freqtrade.main._CONF', default_conf) +# # see https://pypi.python.org/pypi/pytest-mock/ +# # and http://www.voidspace.org.uk/python/mock/patch.html +# # No usage example of simple function mocking, +# # and no documentation of side_effect +# mocker.patch('freqtrade.misc.load_config', new=lambda s, t: {}) +# args = MagicMock() +# args.level = 10 +# #load_config('foo') +# backtesting.start(args) +# +# Check what sideeffect backtstesting has done. +# Probably need to capture standard-output and +# check for the generated report table. diff --git a/freqtrade/tests/test_acl_pair.py b/freqtrade/tests/test_acl_pair.py index 8748d97b8..3cbc9cfa0 100644 --- a/freqtrade/tests/test_acl_pair.py +++ b/freqtrade/tests/test_acl_pair.py @@ -5,13 +5,6 @@ from freqtrade.main import refresh_whitelist # perhaps try to anticipate that by using some python package -def assert_list_equal(l1, l2): - for pair in l1: - assert pair in l2 - for pair in l2: - assert pair in l1 - - def whitelist_conf(): return { "stake_currency": "BTC", @@ -53,7 +46,7 @@ def test_refresh_whitelist(mocker): whitelist = ['BTC_ETH', 'BTC_TKN'] pairslist = conf['exchange']['pair_whitelist'] # Ensure all except those in whitelist are removed - assert_list_equal(whitelist, pairslist) + assert set(whitelist) == set(pairslist) def test_refresh_whitelist_dynamic(mocker): @@ -65,7 +58,7 @@ def test_refresh_whitelist_dynamic(mocker): whitelist = ['BTC_ETH', 'BTC_TKN'] refresh_whitelist(whitelist) pairslist = conf['exchange']['pair_whitelist'] - assert_list_equal(whitelist, pairslist) + assert set(whitelist) == set(pairslist) def test_refresh_whitelist_dynamic_empty(mocker): @@ -78,4 +71,4 @@ def test_refresh_whitelist_dynamic_empty(mocker): conf['exchange']['pair_whitelist'] = [] refresh_whitelist(whitelist) pairslist = conf['exchange']['pair_whitelist'] - assert_list_equal(whitelist, pairslist) + assert set(whitelist) == set(pairslist)