From d5ca77da971c48aaa1d88ef3ff5820ca7ea890fd Mon Sep 17 00:00:00 2001 From: kryofly Date: Wed, 10 Jan 2018 07:40:40 +0100 Subject: [PATCH 1/9] tests for analyze --- freqtrade/analyze.py | 5 ++++ freqtrade/tests/test_analyze.py | 47 +++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/freqtrade/analyze.py b/freqtrade/analyze.py index 4b16550c5..8c6c1e111 100644 --- a/freqtrade/analyze.py +++ b/freqtrade/analyze.py @@ -301,6 +301,7 @@ def get_signal(pair: str, signal: SignalType) -> bool: return False if dataframe.empty: + logger.warning('Empty dataframe for pair %s', pair) return False latest = dataframe.iloc[-1] @@ -308,8 +309,12 @@ def get_signal(pair: str, signal: SignalType) -> bool: # Check if dataframe is out of date signal_date = arrow.get(latest['date']) if signal_date < arrow.now() - timedelta(minutes=10): + logger.warning('Too old dataframe for pair %s', pair) return False + # FIX: 20180109, there could be some confusion because we will make a + # boolean result (execute the action or not depending on the signal). + # But the above checks can also return False, and we hide that. result = latest[signal.value] == 1 logger.debug('%s_trigger: %s (pair=%s, signal=%s)', signal.value, latest['date'], pair, result) return result diff --git a/freqtrade/tests/test_analyze.py b/freqtrade/tests/test_analyze.py index 040c45f26..67a5a54f5 100644 --- a/freqtrade/tests/test_analyze.py +++ b/freqtrade/tests/test_analyze.py @@ -1,8 +1,10 @@ # pragma pylint: disable=missing-docstring,W0621 import json +from functools import reduce from unittest.mock import MagicMock import arrow +import datetime import pytest from pandas import DataFrame @@ -10,6 +12,14 @@ from freqtrade.analyze import parse_ticker_dataframe, populate_buy_trend, popula get_signal, SignalType, populate_sell_trend +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 def result(): with open('freqtrade/tests/testdata/BTC_ETH-1.json') as data_file: @@ -65,6 +75,43 @@ def test_returns_latest_sell_signal(mocker): assert not get_signal('BTC-ETH', SignalType.SELL) +def test_get_signal_empty(mocker, caplog): + mocker.patch('freqtrade.analyze.get_ticker_history', return_value=None) + assert not get_signal('foo', SignalType.BUY) + assert log_has('Empty ticker history for pair foo', + caplog.record_tuples) + + +def test_get_signal_execption_valueerror(mocker, caplog): + mocker.patch('freqtrade.analyze.get_ticker_history', return_value=1) + mocker.patch('freqtrade.analyze.analyze_ticker', + side_effect=ValueError('xyz')) + assert not get_signal('foo', SignalType.BUY) + assert log_has('Unable to analyze ticker for pair foo: xyz', + caplog.record_tuples) + + +# This error should never occur becase analyze_ticker is run first, +# and that function can only add columns, it cant delete all rows from the dataframe +def test_get_signal_empty_dataframe(mocker, caplog): + mocker.patch('freqtrade.analyze.get_ticker_history', return_value=1) + mocker.patch('freqtrade.analyze.analyze_ticker', return_value=DataFrame([])) + assert not get_signal('xyz', SignalType.BUY) + assert log_has('Empty dataframe for pair xyz', + caplog.record_tuples) + + +def test_get_signal_old_dataframe(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 not get_signal('xyz', SignalType.BUY) + assert log_has('Too old dataframe for pair xyz', + caplog.record_tuples) + + def test_get_signal_handles_exceptions(mocker): mocker.patch('freqtrade.analyze.get_ticker_history', return_value=MagicMock()) mocker.patch('freqtrade.analyze.analyze_ticker', From ad2328bbd8392cc3190825e5d5b8c7fc69a4db14 Mon Sep 17 00:00:00 2001 From: kryofly Date: Wed, 10 Jan 2018 07:41:37 +0100 Subject: [PATCH 2/9] tests for exchange --- freqtrade/tests/exchange/test_exchange.py | 87 +++++++++++++++++++++-- 1 file changed, 81 insertions(+), 6 deletions(-) diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 0a900e7c9..20e594ee4 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -7,13 +7,23 @@ 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 + 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): - mocker.patch('freqtrade.exchange.validate_pairs', - side_effect=lambda s: True) - init(config=default_conf) + maybe_init_api(default_conf, mocker) assert ('freqtrade.exchange', logging.INFO, '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 -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() tick = {"success": True, 'result': {'Bid': 0.00001098, 'Ask': 0.00001099, 'Last': 0.0001}} 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) # 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) assert ticker['bid'] == 0.00001098 assert ticker['ask'] == 0.00001099 @@ -187,6 +200,26 @@ def test_get_ticker(mocker, ticker): assert ticker['ask'] == 1 +def test_get_ticker_history(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(pair='BTC_ETH') + 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 + get_ticker_history(pair='BTC_ETH') + assert ticks == 123 + + def test_cancel_order_dry_run(default_conf, mocker): default_conf['dry_run'] = True 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 +# 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): mocker.patch('freqtrade.exchange.validate_pairs', side_effect=lambda s: True) @@ -209,3 +269,18 @@ def test_get_fee(default_conf, mocker): init(default_conf) 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 From f8cc08e2a15c004a23f186004d70c88d7eec21af Mon Sep 17 00:00:00 2001 From: kryofly Date: Wed, 10 Jan 2018 09:09:33 +0100 Subject: [PATCH 3/9] small refactor splitting the _process() --- freqtrade/main.py | 62 ++++++++++++++++++++++----------- freqtrade/tests/conftest.py | 9 +++++ freqtrade/tests/test_analyze.py | 26 +++++--------- freqtrade/tests/test_main.py | 29 +++++++++++++++ 4 files changed, 88 insertions(+), 38 deletions(-) diff --git a/freqtrade/main.py b/freqtrade/main.py index efd0d7c44..4e69e572d 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -72,12 +72,49 @@ def refresh_whitelist(whitelist: List[str]) -> List[str]: return final_list +def process_maybe_execute_buy(conf): + """ + 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'])): + 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): + """ + 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) + return False + + def _process(nb_assets: Optional[int] = 0) -> bool: """ Queries the persistence layer for open trades and handles them, otherwise a new trade is created. :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 try: @@ -95,33 +132,16 @@ def _process(nb_assets: Optional[int] = 0) -> bool: # Query trades from persistence layer trades = Trade.query.filter(Trade.is_open.is_(True)).all() if len(trades) < _CONF['max_open_trades']: - try: - # Create entity and execute trade - state_changed = create_trade(float(_CONF['stake_amount'])) - 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) + state_changed = process_maybe_execute_buy(_CONF) for trade in trades: - # 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 - state_changed = handle_trade(trade) or state_changed + state_changed = process_maybe_execute_sell(trade) or state_changed if 'unfilledtimeout' in _CONF: # Check and handle any timed out open orders check_handle_timedout(_CONF['unfilledtimeout']) - Trade.session.flush() + except (requests.exceptions.RequestException, json.JSONDecodeError) as error: logger.warning( 'Got %s in _process(), retrying in 30 seconds...', diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index 788585345..c93830dd6 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -1,6 +1,7 @@ # pragma pylint: disable=missing-docstring from datetime import datetime from unittest.mock import MagicMock +from functools import reduce import pytest import arrow @@ -10,6 +11,14 @@ from telegram import Message, Chat, Update 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") def default_conf(): """ Returns validated configuration suitable for most tests """ diff --git a/freqtrade/tests/test_analyze.py b/freqtrade/tests/test_analyze.py index 67a5a54f5..17f76ac8e 100644 --- a/freqtrade/tests/test_analyze.py +++ b/freqtrade/tests/test_analyze.py @@ -1,7 +1,7 @@ # pragma pylint: disable=missing-docstring,W0621 import json -from functools import reduce from unittest.mock import MagicMock +import freqtrade.tests.conftest as tt # test tools import arrow import datetime @@ -12,14 +12,6 @@ from freqtrade.analyze import parse_ticker_dataframe, populate_buy_trend, popula get_signal, SignalType, populate_sell_trend -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 def result(): with open('freqtrade/tests/testdata/BTC_ETH-1.json') as data_file: @@ -78,8 +70,8 @@ def test_returns_latest_sell_signal(mocker): def test_get_signal_empty(mocker, caplog): mocker.patch('freqtrade.analyze.get_ticker_history', return_value=None) assert not get_signal('foo', SignalType.BUY) - assert log_has('Empty ticker history for pair foo', - caplog.record_tuples) + assert tt.log_has('Empty ticker history for pair foo', + caplog.record_tuples) def test_get_signal_execption_valueerror(mocker, caplog): @@ -87,8 +79,8 @@ def test_get_signal_execption_valueerror(mocker, caplog): mocker.patch('freqtrade.analyze.analyze_ticker', side_effect=ValueError('xyz')) assert not get_signal('foo', SignalType.BUY) - assert log_has('Unable to analyze ticker for pair foo: xyz', - caplog.record_tuples) + assert tt.log_has('Unable to analyze ticker for pair foo: xyz', + caplog.record_tuples) # This error should never occur becase analyze_ticker is run first, @@ -97,8 +89,8 @@ def test_get_signal_empty_dataframe(mocker, caplog): mocker.patch('freqtrade.analyze.get_ticker_history', return_value=1) mocker.patch('freqtrade.analyze.analyze_ticker', return_value=DataFrame([])) assert not get_signal('xyz', SignalType.BUY) - assert log_has('Empty dataframe for pair xyz', - caplog.record_tuples) + assert tt.log_has('Empty dataframe for pair xyz', + caplog.record_tuples) def test_get_signal_old_dataframe(mocker, caplog): @@ -108,8 +100,8 @@ def test_get_signal_old_dataframe(mocker, caplog): ticks = DataFrame([{'buy': 1, 'date': oldtime}]) mocker.patch('freqtrade.analyze.analyze_ticker', return_value=DataFrame(ticks)) assert not get_signal('xyz', SignalType.BUY) - assert log_has('Too old dataframe for pair xyz', - caplog.record_tuples) + assert tt.log_has('Too old dataframe for pair xyz', + caplog.record_tuples) def test_get_signal_handles_exceptions(mocker): diff --git a/freqtrade/tests/test_main.py b/freqtrade/tests/test_main.py index cceb555f7..110c40862 100644 --- a/freqtrade/tests/test_main.py +++ b/freqtrade/tests/test_main.py @@ -1,6 +1,7 @@ # pragma pylint: disable=missing-docstring,C0103 import copy from unittest.mock import MagicMock +import freqtrade.tests.conftest as tt # test tools import pytest import requests @@ -50,6 +51,34 @@ def test_main_start_hyperopt(mocker): 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) + mocker.patch('freqtrade.main.create_trade', return_value=False) + assert not main.process_maybe_execute_buy(default_conf) + + +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) + 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) + + +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) + tt.log_has('Unable to create trade:', caplog.record_tuples) + + def test_process_trade_creation(default_conf, ticker, limit_buy_order, health, mocker): mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock()) From 0cb57bee0e1eb20dc28bc39ddd4d23c4d5c6dc38 Mon Sep 17 00:00:00 2001 From: kryofly Date: Wed, 10 Jan 2018 10:29:04 +0100 Subject: [PATCH 4/9] small refactor of check_handle_timedout --- freqtrade/main.py | 82 ++++++++++++++++++++++++------------ freqtrade/tests/test_main.py | 46 +++++++++++++++++++- 2 files changed, 100 insertions(+), 28 deletions(-) diff --git a/freqtrade/main.py b/freqtrade/main.py index 4e69e572d..51f87c82d 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -158,6 +158,53 @@ def _process(nb_assets: Optional[int] = 0) -> bool: 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, order): + """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) + 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) + return False + + +# FIX: 20180110, should cancel_order() be cond. or unconditionally called? +def handle_timedout_limit_sell(trade, order): + """ + 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 + 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: """ Check if any orders are timed out and cancel if neccessary @@ -171,35 +218,16 @@ def check_handle_timedout(timeoutvalue: int) -> None: ordertime = arrow.get(order['opened']) if order['type'] == "LIMIT_BUY" and ordertime < timeoutthreashold: - # Buy timeout - cancel 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) - 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) + handle_timedout_limit_buy(trade, order) + elif order['type'] == "LIMIT_SELL" and ordertime < timeoutthreashold: - # Sell timeout - cancel order and update trade - 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 - logger.info('Sell order timeout for %s.', trade) + if handle_timedout_limit_sell(trade, order): + # BUG? if there is more trades that are + # timed out, shouldn't we collect and + # then return all of them? + # Also the function signature is return None. + # But we return True here. return True - else: - # TODO: figure out how to handle partially complete sell orders - pass def execute_sell(trade: Trade, limit: float) -> None: diff --git a/freqtrade/tests/test_main.py b/freqtrade/tests/test_main.py index 110c40862..fc0053f78 100644 --- a/freqtrade/tests/test_main.py +++ b/freqtrade/tests/test_main.py @@ -18,11 +18,11 @@ from freqtrade.misc import get_state, State from freqtrade.persistence import Trade import freqtrade.main as main - # Test that main() can start backtesting or hyperopt. # and also ensure we can pass some specific arguments # argument parsing is done in test_misc.py + def test_parse_args_backtesting(mocker): backtesting_mock = mocker.patch( 'freqtrade.optimize.backtesting.start', MagicMock()) @@ -261,6 +261,20 @@ def test_create_trade_no_pairs_after_blacklist(default_conf, ticker, mocker): create_trade(default_conf['stake_amount']) +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)) + 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) + + def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, mocker): mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True) @@ -414,6 +428,21 @@ def test_check_handle_timedout_buy(default_conf, ticker, health, limit_buy_order 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 = {} + order['remaining'] = 1 + order['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, health, limit_sell_order_old, mocker): mocker.patch.dict('freqtrade.main._CONF', default_conf) cancel_order_mock = MagicMock() @@ -446,6 +475,21 @@ def test_check_handle_timedout_sell(default_conf, ticker, health, limit_sell_ord 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 = {} + order['remaining'] = 1 + order['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, health, mocker): mocker.patch.dict('freqtrade.main._CONF', default_conf) From f848a5c87d3b46298fb2541b30d3fc87fc7d3f4a Mon Sep 17 00:00:00 2001 From: kryofly Date: Wed, 10 Jan 2018 12:00:00 +0100 Subject: [PATCH 5/9] tests optimize load_data --- freqtrade/optimize/__init__.py | 9 +++++++-- freqtrade/tests/optimize/test_optimize.py | 15 +++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/__init__.py b/freqtrade/optimize/__init__.py index 2d73c3215..d76334778 100644 --- a/freqtrade/optimize/__init__.py +++ b/freqtrade/optimize/__init__.py @@ -88,6 +88,12 @@ def download_pairs(datadir, pairs: List[str]) -> bool: 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: """ Download the latest 1 and 5 ticker intervals from Bittrex for the pairs passed in parameters @@ -127,7 +133,6 @@ def download_backtesting_testdata(datadir: str, pair: str, interval: int = 5) -> logger.debug("New End: {}".format(data[-1:][0]['T'])) data = sorted(data, key=lambda data: data['T']) - with open(filename, "wt") as fp: - json.dump(data, fp) + file_dump_json(filename, data) return True diff --git a/freqtrade/tests/optimize/test_optimize.py b/freqtrade/tests/optimize/test_optimize.py index a5892f278..d74b9a224 100644 --- a/freqtrade/tests/optimize/test_optimize.py +++ b/freqtrade/tests/optimize/test_optimize.py @@ -2,6 +2,7 @@ import os import logging +# from unittest.mock import MagicMock from shutil import copyfile from freqtrade import exchange, optimize from freqtrade.exchange import Bittrex @@ -170,7 +171,21 @@ def test_download_backtesting_testdata(default_conf, ticker_history, mocker): _clean_test_file(file2) +def test_download_backtesting_testdata2(default_conf, mocker): + tick = [{'T': 'bar'}, {'T': 'foo'}] + mocker.patch('freqtrade.optimize.__init__.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(): assert not load_tickerdata_file(None, 'BTC_UNITEST', 7) tickerdata = load_tickerdata_file(None, 'BTC_UNITEST', 1) assert _btc_unittest_length == len(tickerdata) + + +def test_init(mocker): + conf = {'exchange': {'pair_whitelist': []}} + mocker.patch('freqtrade.optimize.hyperopt_optimize_conf', return_value=conf) + assert {} == optimize.load_data('', pairs=[], refresh_pairs=True) From 53447e7ef56a2d4e10a6200c5de65490857d6309 Mon Sep 17 00:00:00 2001 From: kryofly Date: Sat, 13 Jan 2018 12:52:02 +0100 Subject: [PATCH 6/9] test cleanup --- freqtrade/tests/test_main.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/freqtrade/tests/test_main.py b/freqtrade/tests/test_main.py index 486c3c83e..93931fe9c 100644 --- a/freqtrade/tests/test_main.py +++ b/freqtrade/tests/test_main.py @@ -432,9 +432,8 @@ def test_handle_timedout_limit_buy(default_conf, mocker): mocker.patch('freqtrade.exchange.cancel_order', cancel_order) Trade.session = MagicMock() trade = MagicMock() - order = {} - order['remaining'] = 1 - order['amount'] = 1 + order = {'remaining': 1, + 'amount': 1} assert main.handle_timedout_limit_buy(trade, order) assert cancel_order.call_count == 1 order['amount'] = 2 @@ -478,9 +477,8 @@ def test_handle_timedout_limit_sell(default_conf, mocker): cancel_order = MagicMock() mocker.patch('freqtrade.exchange.cancel_order', cancel_order) trade = MagicMock() - order = {} - order['remaining'] = 1 - order['amount'] = 1 + order = {'remaining': 1, + 'amount': 1} assert main.handle_timedout_limit_sell(trade, order) assert cancel_order.call_count == 1 order['amount'] = 2 From a62a5f814a3a05017b1e4a13affa558829cbf1bc Mon Sep 17 00:00:00 2001 From: kryofly Date: Sat, 13 Jan 2018 13:16:40 +0100 Subject: [PATCH 7/9] main returns integer instead of sys.exit --- freqtrade/main.py | 7 ++++--- freqtrade/tests/test_main.py | 18 ++++++++++++++---- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/freqtrade/main.py b/freqtrade/main.py index f0b901626..918a0dcc9 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -446,7 +446,7 @@ def cleanup() -> None: 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 :return: None @@ -458,7 +458,7 @@ def main(sysargv=sys.argv[1:]) -> None: # A subcommand has been issued if hasattr(args, 'func'): args.func(args) - exit(0) + return 0 # Initialize logger logging.basicConfig( @@ -514,7 +514,8 @@ def main(sysargv=sys.argv[1:]) -> None: logger.exception('Got fatal exception!') finally: cleanup() + return 0 if __name__ == '__main__': - main() + main(sys.argv[1:]) diff --git a/freqtrade/tests/test_main.py b/freqtrade/tests/test_main.py index 93931fe9c..7b93c623a 100644 --- a/freqtrade/tests/test_main.py +++ b/freqtrade/tests/test_main.py @@ -25,8 +25,7 @@ def test_parse_args_backtesting(mocker): further argument parsing is done in test_misc.py """ backtesting_mock = mocker.patch( 'freqtrade.optimize.backtesting.start', MagicMock()) - with pytest.raises(SystemExit, match=r'0'): - main.main(['backtesting']) + main.main(['backtesting']) assert backtesting_mock.call_count == 1 call_args = backtesting_mock.call_args[0][0] assert call_args.config == 'config.json' @@ -40,8 +39,7 @@ def test_parse_args_backtesting(mocker): def test_main_start_hyperopt(mocker): hyperopt_mock = mocker.patch( 'freqtrade.optimize.hyperopt.start', MagicMock()) - with pytest.raises(SystemExit, match=r'0'): - main.main(['hyperopt']) + main.main(['hyperopt']) assert hyperopt_mock.call_count == 1 call_args = hyperopt_mock.call_args[0][0] assert call_args.config == 'config.json' @@ -50,6 +48,18 @@ def test_main_start_hyperopt(mocker): assert call_args.func is not None +# def test_main_trader(mocker): +# mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock()) +# mocker.patch('freqtrade.misc.get_state', return_value=True) +# mocker.patch.multiple('freqtrade.main', +# init=MagicMock(), +# cleanup=MagicMock(), +# throttle=MagicMock() +# ) +# Cant run this yet because we have an unconditional while loop in main +# assert 0 == main.main([]) + + 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) From fc2e8b321f0b38b62d5962fd5e80c9e2ee17d51e Mon Sep 17 00:00:00 2001 From: kryofly Date: Sat, 13 Jan 2018 14:29:16 +0100 Subject: [PATCH 8/9] test for bittrex to reach 100% cov again --- freqtrade/tests/exchange/test_exchange_bittrex.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/freqtrade/tests/exchange/test_exchange_bittrex.py b/freqtrade/tests/exchange/test_exchange_bittrex.py index e01e7fa02..35d242b55 100644 --- a/freqtrade/tests/exchange/test_exchange_bittrex.py +++ b/freqtrade/tests/exchange/test_exchange_bittrex.py @@ -335,3 +335,8 @@ def test_validate_response_min_trade_requirement_not_met(): } with pytest.raises(ContentDecodingError, match=r'.*MIN_TRADE_REQUIREMENT_NOT_MET.*'): Bittrex._validate_response(response) + + +def test_custom_requests(mocker): + mocker.patch('freqtrade.exchange.bittrex.requests', MagicMock()) + btx.custom_requests('http://', '') From aec481b6b3422f8a6ab5d2299fc795f61c3956f2 Mon Sep 17 00:00:00 2001 From: kryofly Date: Mon, 22 Jan 2018 08:30:00 +0100 Subject: [PATCH 9/9] tests: 100% cov bittrex.py --- freqtrade/tests/exchange/test_exchange_bittrex.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/freqtrade/tests/exchange/test_exchange_bittrex.py b/freqtrade/tests/exchange/test_exchange_bittrex.py index 7addac7ab..949acf25f 100644 --- a/freqtrade/tests/exchange/test_exchange_bittrex.py +++ b/freqtrade/tests/exchange/test_exchange_bittrex.py @@ -143,7 +143,7 @@ def test_exchange_bittrex_fee(): 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() fb = FakeBittrex() uuid = wb.buy('BTC_ETH', 1, 1) @@ -154,7 +154,7 @@ def test_exchange_bittrex_buy_good(mocker): wb.buy('BAD', 1, 1) -def test_exchange_bittrex_sell_good(mocker): +def test_exchange_bittrex_sell_good(): wb = make_wrap_bittrex() fb = FakeBittrex() uuid = wb.sell('BTC_ETH', 1, 1) @@ -165,7 +165,7 @@ def test_exchange_bittrex_sell_good(mocker): uuid = wb.sell('BAD', 1, 1) -def test_exchange_bittrex_get_balance(mocker): +def test_exchange_bittrex_get_balance(): wb = make_wrap_bittrex() fb = FakeBittrex() bal = wb.get_balance('BTC_ETH') @@ -238,10 +238,12 @@ def test_exchange_bittrex_get_ticker_bad(): 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() 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():