Merge branch 'develop' into verify_date_on_new_candle_on_get_signal

This commit is contained in:
hroff-1902
2020-05-19 21:34:58 +03:00
committed by GitHub
133 changed files with 4537 additions and 1958 deletions

View File

@@ -10,11 +10,13 @@ from freqtrade.commands import (start_convert_data, start_create_userdir,
start_list_hyperopts, start_list_markets,
start_list_strategies, start_list_timeframes,
start_new_hyperopt, start_new_strategy,
start_test_pairlist, start_trading)
start_show_trades, start_test_pairlist,
start_trading)
from freqtrade.configuration import setup_utils_configuration
from freqtrade.exceptions import OperationalException
from freqtrade.state import RunMode
from tests.conftest import (get_args, log_has, log_has_re, patch_exchange,
from tests.conftest import (create_mock_trades, get_args, log_has, log_has_re,
patch_exchange,
patched_configuration_load_config_file)
@@ -30,7 +32,7 @@ def test_setup_utils_configuration():
assert config['exchange']['secret'] == ''
def test_start_trading_fail(mocker):
def test_start_trading_fail(mocker, caplog):
mocker.patch("freqtrade.worker.Worker.run", MagicMock(side_effect=OperationalException))
@@ -41,16 +43,15 @@ def test_start_trading_fail(mocker):
'trade',
'-c', 'config.json.example'
]
with pytest.raises(OperationalException):
start_trading(get_args(args))
start_trading(get_args(args))
assert exitmock.call_count == 1
exitmock.reset_mock()
caplog.clear()
mocker.patch("freqtrade.worker.Worker.__init__", MagicMock(side_effect=OperationalException))
with pytest.raises(OperationalException):
start_trading(get_args(args))
start_trading(get_args(args))
assert exitmock.call_count == 0
assert log_has('Fatal exception!', caplog)
def test_list_exchanges(capsys):
@@ -727,7 +728,7 @@ def test_start_test_pairlist(mocker, caplog, tickers, default_conf, capsys):
assert re.match("['ETH/BTC', 'TKN/BTC', 'BLK/BTC', 'LTC/BTC', 'XRP/BTC']", captured.out)
def test_hyperopt_list(mocker, capsys, hyperopt_results):
def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results):
mocker.patch(
'freqtrade.optimize.hyperopt.Hyperopt.load_previous_results',
MagicMock(return_value=hyperopt_results)
@@ -911,8 +912,7 @@ def test_hyperopt_list(mocker, capsys, hyperopt_results):
pargs['config'] = None
start_hyperopt_list(pargs)
captured = capsys.readouterr()
assert all(x in captured.out
for x in ["CSV-File created!"])
log_has("CSV file created: test_file.csv", caplog)
f = Path("test_file.csv")
assert 'Best,1,2,-1.25%,-0.00125625,,-2.51,"3,930.0 m",0.43662' in f.read_text()
assert f.is_file()
@@ -1041,3 +1041,46 @@ def test_convert_data_trades(mocker, testdatadir):
assert trades_mock.call_args[1]['convert_from'] == 'jsongz'
assert trades_mock.call_args[1]['convert_to'] == 'json'
assert trades_mock.call_args[1]['erase'] is False
@pytest.mark.usefixtures("init_persistence")
def test_show_trades(mocker, fee, capsys, caplog):
mocker.patch("freqtrade.persistence.init")
create_mock_trades(fee)
args = [
"show-trades",
"--db-url",
"sqlite:///"
]
pargs = get_args(args)
pargs['config'] = None
start_show_trades(pargs)
assert log_has("Printing 3 Trades: ", caplog)
captured = capsys.readouterr()
assert "Trade(id=1" in captured.out
assert "Trade(id=2" in captured.out
assert "Trade(id=3" in captured.out
args = [
"show-trades",
"--db-url",
"sqlite:///",
"--print-json",
"--trade-ids", "1", "2"
]
pargs = get_args(args)
pargs['config'] = None
start_show_trades(pargs)
captured = capsys.readouterr()
assert log_has("Printing 2 Trades: ", caplog)
assert '"trade_id": 1' in captured.out
assert '"trade_id": 2' in captured.out
assert '"trade_id": 3' not in captured.out
args = [
"show-trades",
]
pargs = get_args(args)
pargs['config'] = None
with pytest.raises(OperationalException, match=r"--db-url is required for this command."):
start_show_trades(pargs)

View File

@@ -15,7 +15,7 @@ from telegram import Chat, Message, Update
from freqtrade import constants, persistence
from freqtrade.commands import Arguments
from freqtrade.data.converter import parse_ticker_dataframe
from freqtrade.data.converter import ohlcv_to_dataframe
from freqtrade.edge import Edge, PairInfo
from freqtrade.exchange import Exchange
from freqtrade.freqtradebot import FreqtradeBot
@@ -92,7 +92,7 @@ def patch_wallet(mocker, free=999.9) -> None:
def patch_whitelist(mocker, conf) -> None:
mocker.patch('freqtrade.freqtradebot.FreqtradeBot._refresh_whitelist',
mocker.patch('freqtrade.freqtradebot.FreqtradeBot._refresh_active_whitelist',
MagicMock(return_value=conf['exchange']['pair_whitelist']))
@@ -166,6 +166,52 @@ def patch_get_signal(freqtrade: FreqtradeBot, value=(True, False)) -> None:
freqtrade.exchange.refresh_latest_ohlcv = lambda p: None
def create_mock_trades(fee):
"""
Create some fake trades ...
"""
# Simulate dry_run entries
trade = Trade(
pair='ETH/BTC',
stake_amount=0.001,
amount=123.0,
fee_open=fee.return_value,
fee_close=fee.return_value,
open_rate=0.123,
exchange='bittrex',
open_order_id='dry_run_buy_12345'
)
Trade.session.add(trade)
trade = Trade(
pair='ETC/BTC',
stake_amount=0.001,
amount=123.0,
fee_open=fee.return_value,
fee_close=fee.return_value,
open_rate=0.123,
close_rate=0.128,
close_profit=0.005,
exchange='bittrex',
is_open=False,
open_order_id='dry_run_sell_12345'
)
Trade.session.add(trade)
# Simulate prod entry
trade = Trade(
pair='ETC/BTC',
stake_amount=0.001,
amount=123.0,
fee_open=fee.return_value,
fee_close=fee.return_value,
open_rate=0.123,
exchange='bittrex',
open_order_id='prod_buy_12345'
)
Trade.session.add(trade)
@pytest.fixture(autouse=True)
def patch_coingekko(mocker) -> None:
"""
@@ -203,6 +249,7 @@ def default_conf(testdatadir):
"fiat_display_currency": "USD",
"ticker_interval": '5m',
"dry_run": True,
"cancel_open_orders_on_exit": False,
"minimal_roi": {
"40": 0.0,
"30": 0.01,
@@ -258,7 +305,8 @@ def default_conf(testdatadir):
"user_data_dir": Path("user_data"),
"verbosity": 3,
"strategy_path": str(Path(__file__).parent / "strategy" / "strats"),
"strategy": "DefaultStrategy"
"strategy": "DefaultStrategy",
"internals": {},
}
return configuration
@@ -693,6 +741,31 @@ def shitcoinmarkets(markets):
"future": False,
"active": True
},
'ADAHALF/USDT': {
"percentage": True,
"tierBased": False,
"taker": 0.001,
"maker": 0.001,
"precision": {
"base": 8,
"quote": 8,
"amount": 2,
"price": 4
},
"limits": {
},
"id": "ADAHALFUSDT",
"symbol": "ADAHALF/USDT",
"base": "ADAHALF",
"quote": "USDT",
"baseId": "ADAHALF",
"quoteId": "USDT",
"info": {},
"type": "spot",
"spot": True,
"future": False,
"active": True
},
})
return shitmarkets
@@ -708,10 +781,11 @@ def limit_buy_order():
'id': 'mocked_limit_buy',
'type': 'limit',
'side': 'buy',
'pair': 'mocked',
'symbol': 'mocked',
'datetime': arrow.utcnow().isoformat(),
'price': 0.00001099,
'amount': 90.99181073,
'filled': 90.99181073,
'remaining': 0.0,
'status': 'closed'
}
@@ -723,10 +797,11 @@ def market_buy_order():
'id': 'mocked_market_buy',
'type': 'market',
'side': 'buy',
'pair': 'mocked',
'symbol': 'mocked',
'datetime': arrow.utcnow().isoformat(),
'price': 0.00004099,
'amount': 91.99181073,
'filled': 91.99181073,
'remaining': 0.0,
'status': 'closed'
}
@@ -738,10 +813,11 @@ def market_sell_order():
'id': 'mocked_limit_sell',
'type': 'market',
'side': 'sell',
'pair': 'mocked',
'symbol': 'mocked',
'datetime': arrow.utcnow().isoformat(),
'price': 0.00004173,
'amount': 91.99181073,
'filled': 91.99181073,
'remaining': 0.0,
'status': 'closed'
}
@@ -753,10 +829,11 @@ def limit_buy_order_old():
'id': 'mocked_limit_buy_old',
'type': 'limit',
'side': 'buy',
'pair': 'mocked',
'symbol': 'mocked',
'datetime': str(arrow.utcnow().shift(minutes=-601).datetime),
'price': 0.00001099,
'amount': 90.99181073,
'filled': 0.0,
'remaining': 90.99181073,
'status': 'open'
}
@@ -768,10 +845,11 @@ def limit_sell_order_old():
'id': 'mocked_limit_sell_old',
'type': 'limit',
'side': 'sell',
'pair': 'ETH/BTC',
'symbol': 'ETH/BTC',
'datetime': arrow.utcnow().shift(minutes=-601).isoformat(),
'price': 0.00001099,
'amount': 90.99181073,
'filled': 0.0,
'remaining': 90.99181073,
'status': 'open'
}
@@ -783,10 +861,11 @@ def limit_buy_order_old_partial():
'id': 'mocked_limit_buy_old_partial',
'type': 'limit',
'side': 'buy',
'pair': 'ETH/BTC',
'symbol': 'ETH/BTC',
'datetime': arrow.utcnow().shift(minutes=-601).isoformat(),
'price': 0.00001099,
'amount': 90.99181073,
'filled': 23.0,
'remaining': 67.99181073,
'status': 'open'
}
@@ -796,10 +875,103 @@ def limit_buy_order_old_partial():
def limit_buy_order_old_partial_canceled(limit_buy_order_old_partial):
res = deepcopy(limit_buy_order_old_partial)
res['status'] = 'canceled'
res['fee'] = {'cost': 0.0001, 'currency': 'ETH'}
res['fee'] = {'cost': 0.023, 'currency': 'ETH'}
return res
@pytest.fixture(scope='function')
def limit_buy_order_canceled_empty(request):
# Indirect fixture
# Documentation:
# https://docs.pytest.org/en/latest/example/parametrize.html#apply-indirect-on-particular-arguments
exchange_name = request.param
if exchange_name == 'ftx':
return {
'info': {},
'id': '1234512345',
'clientOrderId': None,
'timestamp': arrow.utcnow().shift(minutes=-601).timestamp,
'datetime': arrow.utcnow().shift(minutes=-601).isoformat(),
'lastTradeTimestamp': None,
'symbol': 'LTC/USDT',
'type': 'limit',
'side': 'buy',
'price': 34.3225,
'amount': 0.55,
'cost': 0.0,
'average': None,
'filled': 0.0,
'remaining': 0.0,
'status': 'closed',
'fee': None,
'trades': None
}
elif exchange_name == 'kraken':
return {
'info': {},
'id': 'AZNPFF-4AC4N-7MKTAT',
'clientOrderId': None,
'timestamp': arrow.utcnow().shift(minutes=-601).timestamp,
'datetime': arrow.utcnow().shift(minutes=-601).isoformat(),
'lastTradeTimestamp': None,
'status': 'canceled',
'symbol': 'LTC/USDT',
'type': 'limit',
'side': 'buy',
'price': 34.3225,
'cost': 0.0,
'amount': 0.55,
'filled': 0.0,
'average': 0.0,
'remaining': 0.55,
'fee': {'cost': 0.0, 'rate': None, 'currency': 'USDT'},
'trades': []
}
elif exchange_name == 'binance':
return {
'info': {},
'id': '1234512345',
'clientOrderId': 'alb1234123',
'timestamp': arrow.utcnow().shift(minutes=-601).timestamp,
'datetime': arrow.utcnow().shift(minutes=-601).isoformat(),
'lastTradeTimestamp': None,
'symbol': 'LTC/USDT',
'type': 'limit',
'side': 'buy',
'price': 0.016804,
'amount': 0.55,
'cost': 0.0,
'average': None,
'filled': 0.0,
'remaining': 0.55,
'status': 'canceled',
'fee': None,
'trades': None
}
else:
return {
'info': {},
'id': '1234512345',
'clientOrderId': 'alb1234123',
'timestamp': arrow.utcnow().shift(minutes=-601).timestamp,
'datetime': arrow.utcnow().shift(minutes=-601).isoformat(),
'lastTradeTimestamp': None,
'symbol': 'LTC/USDT',
'type': 'limit',
'side': 'buy',
'price': 0.016804,
'amount': 0.55,
'cost': 0.0,
'average': None,
'filled': 0.0,
'remaining': 0.55,
'status': 'canceled',
'fee': None,
'trades': None
}
@pytest.fixture
def limit_sell_order():
return {
@@ -810,6 +982,7 @@ def limit_sell_order():
'datetime': arrow.utcnow().isoformat(),
'price': 0.00001173,
'amount': 90.99181073,
'filled': 90.99181073,
'remaining': 0.0,
'status': 'closed'
}
@@ -849,15 +1022,15 @@ def order_book_l2():
@pytest.fixture
def ticker_history_list():
def ohlcv_history_list():
return [
[
1511686200000, # unix timestamp ms
8.794e-05, # open
8.948e-05, # high
8.794e-05, # low
8.88e-05, # close
0.0877869, # volume (in quote currency)
8.794e-05, # open
8.948e-05, # high
8.794e-05, # low
8.88e-05, # close
0.0877869, # volume (in quote currency)
],
[
1511686500000,
@@ -879,8 +1052,9 @@ def ticker_history_list():
@pytest.fixture
def ticker_history(ticker_history_list):
return parse_ticker_dataframe(ticker_history_list, "5m", pair="UNITTEST/BTC", fill_missing=True)
def ohlcv_history(ohlcv_history_list):
return ohlcv_to_dataframe(ohlcv_history_list, "5m", pair="UNITTEST/BTC",
fill_missing=True)
@pytest.fixture
@@ -1189,14 +1363,37 @@ def tickers():
"quoteVolume": 323652.075405,
"info": {}
},
# Example of leveraged pair with incomplete info
"ADAHALF/USDT": {
"symbol": "ADAHALF/USDT",
"timestamp": 1580469388244,
"datetime": "2020-01-31T11:16:28.244Z",
"high": None,
"low": None,
"bid": 0.7305,
"bidVolume": None,
"ask": 0.7342,
"askVolume": None,
"vwap": None,
"open": None,
"close": None,
"last": None,
"previousClose": None,
"change": None,
"percentage": 2.628,
"average": None,
"baseVolume": 0.0,
"quoteVolume": 0.0,
"info": {}
},
})
@pytest.fixture
def result(testdatadir):
with (testdatadir / 'UNITTEST_BTC-1m.json').open('r') as data_file:
return parse_ticker_dataframe(json.load(data_file), '1m', pair="UNITTEST/BTC",
fill_missing=True)
return ohlcv_to_dataframe(json.load(data_file), '1m', pair="UNITTEST/BTC",
fill_missing=True)
@pytest.fixture(scope="function")
@@ -1226,6 +1423,15 @@ def trades_for_order():
@pytest.fixture(scope="function")
def trades_history():
return [[1565798399463, '126181329', None, 'buy', 0.019627, 0.04, 0.00078508],
[1565798399629, '126181330', None, 'buy', 0.019627, 0.244, 0.004788987999999999],
[1565798399752, '126181331', None, 'sell', 0.019626, 0.011, 0.00021588599999999999],
[1565798399862, '126181332', None, 'sell', 0.019626, 0.011, 0.00021588599999999999],
[1565798399872, '126181333', None, 'sell', 0.019626, 0.011, 0.00021588599999999999]]
@pytest.fixture(scope="function")
def fetch_trades_result():
return [{'info': {'a': 126181329,
'p': '0.01962700',
'q': '0.04000000',
@@ -1380,7 +1586,7 @@ def buy_order_fee():
'id': 'mocked_limit_buy_old',
'type': 'limit',
'side': 'buy',
'pair': 'mocked',
'symbol': 'mocked',
'datetime': str(arrow.utcnow().shift(minutes=-601).datetime),
'price': 0.245441,
'amount': 8.0,
@@ -1499,7 +1705,7 @@ def hyperopt_results():
{
'loss': 0.4366182531161519,
'params_dict': {
'mfi-value': 15, 'fastd-value': 20, 'adx-value': 25, 'rsi-value': 28, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 88, 'sell-fastd-value': 97, 'sell-adx-value': 51, 'sell-rsi-value': 67, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1190, 'roi_t2': 541, 'roi_t3': 408, 'roi_p1': 0.026035863879169705, 'roi_p2': 0.12508730043628782, 'roi_p3': 0.27766427921605896, 'stoploss': -0.2562930402099556}, # noqa: E501
'mfi-value': 15, 'fastd-value': 20, 'adx-value': 25, 'rsi-value': 28, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 88, 'sell-fastd-value': 97, 'sell-adx-value': 51, 'sell-rsi-value': 67, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1190, 'roi_t2': 541, 'roi_t3': 408, 'roi_p1': 0.026035863879169705, 'roi_p2': 0.12508730043628782, 'roi_p3': 0.27766427921605896, 'stoploss': -0.2562930402099556}, # noqa: E501
'params_details': {'buy': {'mfi-value': 15, 'fastd-value': 20, 'adx-value': 25, 'rsi-value': 28, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 88, 'sell-fastd-value': 97, 'sell-adx-value': 51, 'sell-rsi-value': 67, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.4287874435315165, 408: 0.15112316431545753, 949: 0.026035863879169705, 2139: 0}, 'stoploss': {'stoploss': -0.2562930402099556}}, # noqa: E501
'results_metrics': {'trade_count': 2, 'avg_profit': -1.254995, 'total_profit': -0.00125625, 'profit': -2.50999, 'duration': 3930.0}, # noqa: E501
'results_explanation': ' 2 trades. Avg profit -1.25%. Total profit -0.00125625 BTC ( -2.51Σ%). Avg duration 3930.0 min.', # noqa: E501
@@ -1510,11 +1716,12 @@ def hyperopt_results():
}, {
'loss': 20.0,
'params_dict': {
'mfi-value': 17, 'fastd-value': 38, 'adx-value': 48, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 96, 'sell-fastd-value': 68, 'sell-adx-value': 63, 'sell-rsi-value': 81, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal', 'roi_t1': 334, 'roi_t2': 683, 'roi_t3': 140, 'roi_p1': 0.06403981740598495, 'roi_p2': 0.055519840060645045, 'roi_p3': 0.3253712811342459, 'stoploss': -0.338070047333259}, # noqa: E501
'params_details': {'buy': {'mfi-value': 17, 'fastd-value': 38, 'adx-value': 48, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, # noqa: E501
'sell': {'sell-mfi-value': 96, 'sell-fastd-value': 68, 'sell-adx-value': 63, 'sell-rsi-value': 81, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal'}, # noqa: E501
'roi': {0: 0.4449309386008759, 140: 0.11955965746663, 823: 0.06403981740598495, 1157: 0}, # noqa: E501
'stoploss': {'stoploss': -0.338070047333259}},
'mfi-value': 17, 'fastd-value': 38, 'adx-value': 48, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 96, 'sell-fastd-value': 68, 'sell-adx-value': 63, 'sell-rsi-value': 81, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal', 'roi_t1': 334, 'roi_t2': 683, 'roi_t3': 140, 'roi_p1': 0.06403981740598495, 'roi_p2': 0.055519840060645045, 'roi_p3': 0.3253712811342459, 'stoploss': -0.338070047333259}, # noqa: E501
'params_details': {
'buy': {'mfi-value': 17, 'fastd-value': 38, 'adx-value': 48, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, # noqa: E501
'sell': {'sell-mfi-value': 96, 'sell-fastd-value': 68, 'sell-adx-value': 63, 'sell-rsi-value': 81, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal'}, # noqa: E501
'roi': {0: 0.4449309386008759, 140: 0.11955965746663, 823: 0.06403981740598495, 1157: 0}, # noqa: E501
'stoploss': {'stoploss': -0.338070047333259}},
'results_metrics': {'trade_count': 1, 'avg_profit': 0.12357, 'total_profit': 6.185e-05, 'profit': 0.12357, 'duration': 1200.0}, # noqa: E501
'results_explanation': ' 1 trades. Avg profit 0.12%. Total profit 0.00006185 BTC ( 0.12Σ%). Avg duration 1200.0 min.', # noqa: E501
'total_profit': 6.185e-05,
@@ -1561,8 +1768,9 @@ def hyperopt_results():
}, {
'loss': 4.713497421432944,
'params_dict': {'mfi-value': 13, 'fastd-value': 41, 'adx-value': 21, 'rsi-value': 29, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower', 'sell-mfi-value': 99, 'sell-fastd-value': 60, 'sell-adx-value': 81, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 771, 'roi_t2': 620, 'roi_t3': 145, 'roi_p1': 0.0586919200378493, 'roi_p2': 0.04984118697312542, 'roi_p3': 0.37521058680247044, 'stoploss': -0.14613268022709905}, # noqa: E501
'params_details': {'buy': {'mfi-value': 13, 'fastd-value': 41, 'adx-value': 21, 'rsi-value': 29, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower'}, 'sell': {'sell-mfi-value': 99, 'sell-fastd-value': 60, 'sell-adx-value': 81, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.4837436938134452, 145: 0.10853310701097472, 765: 0.0586919200378493, 1536: 0}, # noqa: E501
'stoploss': {'stoploss': -0.14613268022709905}}, # noqa: E501
'params_details': {
'buy': {'mfi-value': 13, 'fastd-value': 41, 'adx-value': 21, 'rsi-value': 29, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower'}, 'sell': {'sell-mfi-value': 99, 'sell-fastd-value': 60, 'sell-adx-value': 81, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.4837436938134452, 145: 0.10853310701097472, 765: 0.0586919200378493, 1536: 0}, # noqa: E501
'stoploss': {'stoploss': -0.14613268022709905}}, # noqa: E501
'results_metrics': {'trade_count': 318, 'avg_profit': -0.39833954716981146, 'total_profit': -0.06339929, 'profit': -126.67197600000004, 'duration': 3140.377358490566}, # noqa: E501
'results_explanation': ' 318 trades. Avg profit -0.40%. Total profit -0.06339929 BTC (-126.67Σ%). Avg duration 3140.4 min.', # noqa: E501
'total_profit': -0.06339929,

View File

@@ -1,20 +1,21 @@
from pathlib import Path
from unittest.mock import MagicMock
import pytest
from arrow import Arrow
from pandas import DataFrame, DateOffset, to_datetime, Timestamp
from pandas import DataFrame, DateOffset, Timestamp, to_datetime
from freqtrade.configuration import TimeRange
from freqtrade.data.btanalysis import (BT_DATA_COLUMNS,
analyze_trade_parallelism,
calculate_max_drawdown,
combine_tickers_with_mean,
combine_dataframes_with_mean,
create_cum_profit,
extract_trades_of_period,
load_backtest_data, load_trades,
load_trades_from_db)
from freqtrade.data.history import load_data, load_pair_history
from tests.test_persistence import create_mock_trades
from tests.conftest import create_mock_trades
def test_load_backtest_data(testdatadir):
@@ -104,6 +105,7 @@ def test_load_trades(default_conf, mocker):
load_trades("DB",
db_url=default_conf.get('db_url'),
exportfilename=default_conf.get('exportfilename'),
no_trades=False
)
assert db_mock.call_count == 1
@@ -111,22 +113,32 @@ def test_load_trades(default_conf, mocker):
db_mock.reset_mock()
bt_mock.reset_mock()
default_conf['exportfilename'] = "testfile.json"
default_conf['exportfilename'] = Path("testfile.json")
load_trades("file",
db_url=default_conf.get('db_url'),
exportfilename=default_conf.get('exportfilename'),)
exportfilename=default_conf.get('exportfilename'),
)
assert db_mock.call_count == 0
assert bt_mock.call_count == 1
db_mock.reset_mock()
bt_mock.reset_mock()
default_conf['exportfilename'] = "testfile.json"
load_trades("file",
db_url=default_conf.get('db_url'),
exportfilename=default_conf.get('exportfilename'),
no_trades=True
)
def test_combine_tickers_with_mean(testdatadir):
assert db_mock.call_count == 0
assert bt_mock.call_count == 0
def test_combine_dataframes_with_mean(testdatadir):
pairs = ["ETH/BTC", "ADA/BTC"]
tickers = load_data(datadir=testdatadir,
pairs=pairs,
timeframe='5m'
)
df = combine_tickers_with_mean(tickers)
data = load_data(datadir=testdatadir, pairs=pairs, timeframe='5m')
df = combine_dataframes_with_mean(data)
assert isinstance(df, DataFrame)
assert "ETH/BTC" in df.columns
assert "ADA/BTC" in df.columns
@@ -179,3 +191,28 @@ def test_calculate_max_drawdown(testdatadir):
assert low == Timestamp('2018-01-30 04:45:00', tz='UTC')
with pytest.raises(ValueError, match='Trade dataframe empty.'):
drawdown, h, low = calculate_max_drawdown(DataFrame())
def test_calculate_max_drawdown2():
values = [0.011580, 0.010048, 0.011340, 0.012161, 0.010416, 0.010009, 0.020024,
-0.024662, -0.022350, 0.020496, -0.029859, -0.030511, 0.010041, 0.010872,
-0.025782, 0.010400, 0.012374, 0.012467, 0.114741, 0.010303, 0.010088,
-0.033961, 0.010680, 0.010886, -0.029274, 0.011178, 0.010693, 0.010711]
dates = [Arrow(2020, 1, 1).shift(days=i) for i in range(len(values))]
df = DataFrame(zip(values, dates), columns=['profit', 'open_time'])
# sort by profit and reset index
df = df.sort_values('profit').reset_index(drop=True)
df1 = df.copy()
drawdown, h, low = calculate_max_drawdown(df, date_col='open_time', value_col='profit')
# Ensure df has not been altered.
assert df.equals(df1)
assert isinstance(drawdown, float)
# High must be before low
assert h < low
assert drawdown == 0.091755
df = DataFrame(zip(values[:5], dates[:5]), columns=['profit', 'open_time'])
with pytest.raises(ValueError, match='No losing trade, therefore no drawdown.'):
calculate_max_drawdown(df, date_col='open_time', value_col='profit')

View File

@@ -5,7 +5,8 @@ from freqtrade.configuration.timerange import TimeRange
from freqtrade.data.converter import (convert_ohlcv_format,
convert_trades_format,
ohlcv_fill_up_missing_data,
parse_ticker_dataframe, trim_dataframe)
ohlcv_to_dataframe, trades_dict_to_list,
trades_remove_duplicates, trim_dataframe)
from freqtrade.data.history import (get_timerange, load_data,
load_pair_history, validate_backtest_data)
from tests.conftest import log_has
@@ -16,15 +17,15 @@ def test_dataframe_correct_columns(result):
assert result.columns.tolist() == ['date', 'open', 'high', 'low', 'close', 'volume']
def test_parse_ticker_dataframe(ticker_history_list, caplog):
def test_ohlcv_to_dataframe(ohlcv_history_list, caplog):
columns = ['date', 'open', 'high', 'low', 'close', 'volume']
caplog.set_level(logging.DEBUG)
# Test file with BV data
dataframe = parse_ticker_dataframe(ticker_history_list, '5m',
pair="UNITTEST/BTC", fill_missing=True)
dataframe = ohlcv_to_dataframe(ohlcv_history_list, '5m', pair="UNITTEST/BTC",
fill_missing=True)
assert dataframe.columns.tolist() == columns
assert log_has('Parsing tickerlist to dataframe', caplog)
assert log_has('Converting candle (OHLCV) data to dataframe for pair UNITTEST/BTC.', caplog)
def test_ohlcv_fill_up_missing_data(testdatadir, caplog):
@@ -84,7 +85,8 @@ def test_ohlcv_fill_up_missing_data2(caplog):
]
# Generate test-data without filling missing
data = parse_ticker_dataframe(ticks, timeframe, pair="UNITTEST/BTC", fill_missing=False)
data = ohlcv_to_dataframe(ticks, timeframe, pair="UNITTEST/BTC",
fill_missing=False)
assert len(data) == 3
caplog.set_level(logging.DEBUG)
data2 = ohlcv_fill_up_missing_data(data, timeframe, "UNITTEST/BTC")
@@ -140,14 +142,14 @@ def test_ohlcv_drop_incomplete(caplog):
]
]
caplog.set_level(logging.DEBUG)
data = parse_ticker_dataframe(ticks, timeframe, pair="UNITTEST/BTC",
fill_missing=False, drop_incomplete=False)
data = ohlcv_to_dataframe(ticks, timeframe, pair="UNITTEST/BTC",
fill_missing=False, drop_incomplete=False)
assert len(data) == 4
assert not log_has("Dropping last candle", caplog)
# Drop last candle
data = parse_ticker_dataframe(ticks, timeframe, pair="UNITTEST/BTC",
fill_missing=False, drop_incomplete=True)
data = ohlcv_to_dataframe(ticks, timeframe, pair="UNITTEST/BTC",
fill_missing=False, drop_incomplete=True)
assert len(data) == 3
assert log_has("Dropping last candle", caplog)
@@ -193,32 +195,60 @@ def test_trim_dataframe(testdatadir) -> None:
assert all(data_modify.iloc[0] == data.iloc[25])
def test_convert_trades_format(mocker, default_conf, testdatadir):
file = testdatadir / "XRP_ETH-trades.json.gz"
file_new = testdatadir / "XRP_ETH-trades.json"
_backup_file(file, copy_file=True)
default_conf['datadir'] = testdatadir
def test_trades_remove_duplicates(trades_history):
trades_history1 = trades_history * 3
assert len(trades_history1) == len(trades_history) * 3
res = trades_remove_duplicates(trades_history1)
assert len(res) == len(trades_history)
for i, t in enumerate(res):
assert t == trades_history[i]
assert not file_new.exists()
def test_trades_dict_to_list(fetch_trades_result):
res = trades_dict_to_list(fetch_trades_result)
assert isinstance(res, list)
assert isinstance(res[0], list)
for i, t in enumerate(res):
assert t[0] == fetch_trades_result[i]['timestamp']
assert t[1] == fetch_trades_result[i]['id']
assert t[2] == fetch_trades_result[i]['type']
assert t[3] == fetch_trades_result[i]['side']
assert t[4] == fetch_trades_result[i]['price']
assert t[5] == fetch_trades_result[i]['amount']
assert t[6] == fetch_trades_result[i]['cost']
def test_convert_trades_format(mocker, default_conf, testdatadir):
files = [{'old': testdatadir / "XRP_ETH-trades.json.gz",
'new': testdatadir / "XRP_ETH-trades.json"},
{'old': testdatadir / "XRP_OLD-trades.json.gz",
'new': testdatadir / "XRP_OLD-trades.json"},
]
for file in files:
_backup_file(file['old'], copy_file=True)
assert not file['new'].exists()
default_conf['datadir'] = testdatadir
convert_trades_format(default_conf, convert_from='jsongz',
convert_to='json', erase=False)
assert file_new.exists()
assert file.exists()
for file in files:
assert file['new'].exists()
assert file['old'].exists()
# Remove original file
file.unlink()
# Remove original file
file['old'].unlink()
# Convert back
convert_trades_format(default_conf, convert_from='json',
convert_to='jsongz', erase=True)
for file in files:
assert file['old'].exists()
assert not file['new'].exists()
assert file.exists()
assert not file_new.exists()
_clean_test_file(file)
if file_new.exists():
file_new.unlink()
_clean_test_file(file['old'])
if file['new'].exists():
file['new'].unlink()
def test_convert_ohlcv_format(mocker, default_conf, testdatadir):

View File

@@ -1,25 +1,28 @@
from unittest.mock import MagicMock
from pandas import DataFrame
import pytest
from freqtrade.data.dataprovider import DataProvider
from freqtrade.pairlist.pairlistmanager import PairListManager
from freqtrade.exceptions import DependencyException, OperationalException
from freqtrade.state import RunMode
from tests.conftest import get_patched_exchange
def test_ohlcv(mocker, default_conf, ticker_history):
def test_ohlcv(mocker, default_conf, ohlcv_history):
default_conf["runmode"] = RunMode.DRY_RUN
timeframe = default_conf["ticker_interval"]
exchange = get_patched_exchange(mocker, default_conf)
exchange._klines[("XRP/BTC", timeframe)] = ticker_history
exchange._klines[("UNITTEST/BTC", timeframe)] = ticker_history
exchange._klines[("XRP/BTC", timeframe)] = ohlcv_history
exchange._klines[("UNITTEST/BTC", timeframe)] = ohlcv_history
dp = DataProvider(default_conf, exchange)
assert dp.runmode == RunMode.DRY_RUN
assert ticker_history.equals(dp.ohlcv("UNITTEST/BTC", timeframe))
assert ohlcv_history.equals(dp.ohlcv("UNITTEST/BTC", timeframe))
assert isinstance(dp.ohlcv("UNITTEST/BTC", timeframe), DataFrame)
assert dp.ohlcv("UNITTEST/BTC", timeframe) is not ticker_history
assert dp.ohlcv("UNITTEST/BTC", timeframe, copy=False) is ticker_history
assert dp.ohlcv("UNITTEST/BTC", timeframe) is not ohlcv_history
assert dp.ohlcv("UNITTEST/BTC", timeframe, copy=False) is ohlcv_history
assert not dp.ohlcv("UNITTEST/BTC", timeframe).empty
assert dp.ohlcv("NONESENSE/AAA", timeframe).empty
@@ -37,8 +40,8 @@ def test_ohlcv(mocker, default_conf, ticker_history):
assert dp.ohlcv("UNITTEST/BTC", timeframe).empty
def test_historic_ohlcv(mocker, default_conf, ticker_history):
historymock = MagicMock(return_value=ticker_history)
def test_historic_ohlcv(mocker, default_conf, ohlcv_history):
historymock = MagicMock(return_value=ohlcv_history)
mocker.patch("freqtrade.data.dataprovider.load_pair_history", historymock)
dp = DataProvider(default_conf, None)
@@ -48,24 +51,24 @@ def test_historic_ohlcv(mocker, default_conf, ticker_history):
assert historymock.call_args_list[0][1]["timeframe"] == "5m"
def test_get_pair_dataframe(mocker, default_conf, ticker_history):
def test_get_pair_dataframe(mocker, default_conf, ohlcv_history):
default_conf["runmode"] = RunMode.DRY_RUN
ticker_interval = default_conf["ticker_interval"]
exchange = get_patched_exchange(mocker, default_conf)
exchange._klines[("XRP/BTC", ticker_interval)] = ticker_history
exchange._klines[("UNITTEST/BTC", ticker_interval)] = ticker_history
exchange._klines[("XRP/BTC", ticker_interval)] = ohlcv_history
exchange._klines[("UNITTEST/BTC", ticker_interval)] = ohlcv_history
dp = DataProvider(default_conf, exchange)
assert dp.runmode == RunMode.DRY_RUN
assert ticker_history.equals(dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval))
assert ohlcv_history.equals(dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval))
assert isinstance(dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval), DataFrame)
assert dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval) is not ticker_history
assert dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval) is not ohlcv_history
assert not dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval).empty
assert dp.get_pair_dataframe("NONESENSE/AAA", ticker_interval).empty
# Test with and without parameter
assert dp.get_pair_dataframe("UNITTEST/BTC",
ticker_interval).equals(dp.get_pair_dataframe("UNITTEST/BTC"))
assert dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval)\
.equals(dp.get_pair_dataframe("UNITTEST/BTC"))
default_conf["runmode"] = RunMode.LIVE
dp = DataProvider(default_conf, exchange)
@@ -73,7 +76,7 @@ def test_get_pair_dataframe(mocker, default_conf, ticker_history):
assert isinstance(dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval), DataFrame)
assert dp.get_pair_dataframe("NONESENSE/AAA", ticker_interval).empty
historymock = MagicMock(return_value=ticker_history)
historymock = MagicMock(return_value=ohlcv_history)
mocker.patch("freqtrade.data.dataprovider.load_pair_history", historymock)
default_conf["runmode"] = RunMode.BACKTEST
dp = DataProvider(default_conf, exchange)
@@ -82,21 +85,18 @@ def test_get_pair_dataframe(mocker, default_conf, ticker_history):
# assert dp.get_pair_dataframe("NONESENSE/AAA", ticker_interval).empty
def test_available_pairs(mocker, default_conf, ticker_history):
def test_available_pairs(mocker, default_conf, ohlcv_history):
exchange = get_patched_exchange(mocker, default_conf)
ticker_interval = default_conf["ticker_interval"]
exchange._klines[("XRP/BTC", ticker_interval)] = ticker_history
exchange._klines[("UNITTEST/BTC", ticker_interval)] = ticker_history
exchange._klines[("XRP/BTC", ticker_interval)] = ohlcv_history
exchange._klines[("UNITTEST/BTC", ticker_interval)] = ohlcv_history
dp = DataProvider(default_conf, exchange)
assert len(dp.available_pairs) == 2
assert dp.available_pairs == [
("XRP/BTC", ticker_interval),
("UNITTEST/BTC", ticker_interval),
]
assert dp.available_pairs == [("XRP/BTC", ticker_interval), ("UNITTEST/BTC", ticker_interval), ]
def test_refresh(mocker, default_conf, ticker_history):
def test_refresh(mocker, default_conf, ohlcv_history):
refresh_mock = MagicMock()
mocker.patch("freqtrade.exchange.Exchange.refresh_latest_ohlcv", refresh_mock)
@@ -152,3 +152,45 @@ def test_market(mocker, default_conf, markets):
res = dp.market('UNITTEST/BTC')
assert res is None
def test_ticker(mocker, default_conf, tickers):
ticker_mock = MagicMock(return_value=tickers()['ETH/BTC'])
mocker.patch("freqtrade.exchange.Exchange.fetch_ticker", ticker_mock)
exchange = get_patched_exchange(mocker, default_conf)
dp = DataProvider(default_conf, exchange)
res = dp.ticker('ETH/BTC')
assert type(res) is dict
assert 'symbol' in res
assert res['symbol'] == 'ETH/BTC'
ticker_mock = MagicMock(side_effect=DependencyException('Pair not found'))
mocker.patch("freqtrade.exchange.Exchange.fetch_ticker", ticker_mock)
exchange = get_patched_exchange(mocker, default_conf)
dp = DataProvider(default_conf, exchange)
res = dp.ticker('UNITTEST/BTC')
assert res == {}
def test_current_whitelist(mocker, default_conf, tickers):
# patch default conf to volumepairlist
default_conf['pairlists'][0] = {'method': 'VolumePairList', "number_assets": 5}
mocker.patch.multiple('freqtrade.exchange.Exchange',
exchange_has=MagicMock(return_value=True),
get_tickers=tickers)
exchange = get_patched_exchange(mocker, default_conf)
pairlist = PairListManager(exchange, default_conf)
dp = DataProvider(default_conf, exchange, pairlist)
# Simulate volumepairs from exchange.
pairlist.refresh_pairlist()
assert dp.current_whitelist() == pairlist._whitelist
# The identity of the 2 lists should be identical
assert dp.current_whitelist() is pairlist._whitelist
with pytest.raises(OperationalException):
dp = DataProvider(default_conf, exchange)
dp.current_whitelist()

View File

@@ -12,7 +12,7 @@ from pandas import DataFrame
from pandas.testing import assert_frame_equal
from freqtrade.configuration import TimeRange
from freqtrade.data.converter import parse_ticker_dataframe
from freqtrade.data.converter import ohlcv_to_dataframe
from freqtrade.data.history.history_utils import (
_download_pair_history, _download_trades_history,
_load_cached_data_for_updating, convert_trades_to_ohlcv, get_timerange,
@@ -63,7 +63,7 @@ def _clean_test_file(file: Path) -> None:
file_swp.rename(file)
def test_load_data_30min_ticker(mocker, caplog, default_conf, testdatadir) -> None:
def test_load_data_30min_timeframe(mocker, caplog, default_conf, testdatadir) -> None:
ld = load_pair_history(pair='UNITTEST/BTC', timeframe='30m', datadir=testdatadir)
assert isinstance(ld, DataFrame)
assert not log_has(
@@ -72,7 +72,7 @@ def test_load_data_30min_ticker(mocker, caplog, default_conf, testdatadir) -> No
)
def test_load_data_7min_ticker(mocker, caplog, default_conf, testdatadir) -> None:
def test_load_data_7min_timeframe(mocker, caplog, default_conf, testdatadir) -> None:
ld = load_pair_history(pair='UNITTEST/BTC', timeframe='7m', datadir=testdatadir)
assert isinstance(ld, DataFrame)
assert ld.empty
@@ -82,8 +82,8 @@ def test_load_data_7min_ticker(mocker, caplog, default_conf, testdatadir) -> Non
)
def test_load_data_1min_ticker(ticker_history, mocker, caplog, testdatadir) -> None:
mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=ticker_history)
def test_load_data_1min_timeframe(ohlcv_history, mocker, caplog, testdatadir) -> None:
mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=ohlcv_history)
file = testdatadir / 'UNITTEST_BTC-1m.json'
_backup_file(file, copy_file=True)
load_data(datadir=testdatadir, timeframe='1m', pairs=['UNITTEST/BTC'])
@@ -110,12 +110,12 @@ def test_load_data_startup_candles(mocker, caplog, default_conf, testdatadir) ->
assert ltfmock.call_args_list[0][1]['timerange'].startts == timerange.startts - 20 * 60
def test_load_data_with_new_pair_1min(ticker_history_list, mocker, caplog,
def test_load_data_with_new_pair_1min(ohlcv_history_list, mocker, caplog,
default_conf, testdatadir) -> None:
"""
Test load_pair_history() with 1 min ticker
Test load_pair_history() with 1 min timeframe
"""
mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=ticker_history_list)
mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=ohlcv_history_list)
exchange = get_patched_exchange(mocker, default_conf)
file = testdatadir / 'MEME_BTC-1m.json'
@@ -188,8 +188,8 @@ def test_load_cached_data_for_updating(mocker, testdatadir) -> None:
with open(test_filename, "rt") as file:
test_data = json.load(file)
test_data_df = parse_ticker_dataframe(test_data, '1m', 'UNITTEST/BTC',
fill_missing=False, drop_incomplete=False)
test_data_df = ohlcv_to_dataframe(test_data, '1m', 'UNITTEST/BTC',
fill_missing=False, drop_incomplete=False)
# now = last cached item + 1 hour
now_ts = test_data[-1][0] / 1000 + 60 * 60
mocker.patch('arrow.utcnow', return_value=arrow.get(now_ts))
@@ -230,8 +230,8 @@ def test_load_cached_data_for_updating(mocker, testdatadir) -> None:
assert start_ts is None
def test_download_pair_history(ticker_history_list, mocker, default_conf, testdatadir) -> None:
mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=ticker_history_list)
def test_download_pair_history(ohlcv_history_list, mocker, default_conf, testdatadir) -> None:
mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=ohlcv_history_list)
exchange = get_patched_exchange(mocker, default_conf)
file1_1 = testdatadir / 'MEME_BTC-1m.json'
file1_5 = testdatadir / 'MEME_BTC-5m.json'
@@ -293,7 +293,7 @@ def test_download_pair_history2(mocker, default_conf, testdatadir) -> None:
assert json_dump_mock.call_count == 2
def test_download_backtesting_data_exception(ticker_history, mocker, caplog,
def test_download_backtesting_data_exception(ohlcv_history, mocker, caplog,
default_conf, testdatadir) -> None:
mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv',
side_effect=Exception('File Error'))
@@ -321,15 +321,15 @@ def test_load_partial_missing(testdatadir, caplog) -> None:
# Make sure we start fresh - test missing data at start
start = arrow.get('2018-01-01T00:00:00')
end = arrow.get('2018-01-11T00:00:00')
tickerdata = load_data(testdatadir, '5m', ['UNITTEST/BTC'], startup_candles=20,
timerange=TimeRange('date', 'date', start.timestamp, end.timestamp))
data = load_data(testdatadir, '5m', ['UNITTEST/BTC'], startup_candles=20,
timerange=TimeRange('date', 'date', start.timestamp, end.timestamp))
assert log_has(
'Using indicator startup period: 20 ...', caplog
)
# timedifference in 5 minutes
td = ((end - start).total_seconds() // 60 // 5) + 1
assert td != len(tickerdata['UNITTEST/BTC'])
start_real = tickerdata['UNITTEST/BTC'].iloc[0, 0]
assert td != len(data['UNITTEST/BTC'])
start_real = data['UNITTEST/BTC'].iloc[0, 0]
assert log_has(f'Missing data at start for pair '
f'UNITTEST/BTC, data starts at {start_real.strftime("%Y-%m-%d %H:%M:%S")}',
caplog)
@@ -337,14 +337,14 @@ def test_load_partial_missing(testdatadir, caplog) -> None:
caplog.clear()
start = arrow.get('2018-01-10T00:00:00')
end = arrow.get('2018-02-20T00:00:00')
tickerdata = load_data(datadir=testdatadir, timeframe='5m', pairs=['UNITTEST/BTC'],
timerange=TimeRange('date', 'date', start.timestamp, end.timestamp))
data = load_data(datadir=testdatadir, timeframe='5m', pairs=['UNITTEST/BTC'],
timerange=TimeRange('date', 'date', start.timestamp, end.timestamp))
# timedifference in 5 minutes
td = ((end - start).total_seconds() // 60 // 5) + 1
assert td != len(tickerdata['UNITTEST/BTC'])
assert td != len(data['UNITTEST/BTC'])
# Shift endtime with +5 - as last candle is dropped (partial candle)
end_real = arrow.get(tickerdata['UNITTEST/BTC'].iloc[-1, 0]).shift(minutes=5)
end_real = arrow.get(data['UNITTEST/BTC'].iloc[-1, 0]).shift(minutes=5)
assert log_has(f'Missing data at end for pair '
f'UNITTEST/BTC, data ends at {end_real.strftime("%Y-%m-%d %H:%M:%S")}',
caplog)
@@ -403,7 +403,7 @@ def test_get_timerange(default_conf, mocker, testdatadir) -> None:
default_conf.update({'strategy': 'DefaultStrategy'})
strategy = StrategyResolver.load_strategy(default_conf)
data = strategy.tickerdata_to_dataframe(
data = strategy.ohlcvdata_to_dataframe(
load_data(
datadir=testdatadir,
timeframe='1m',
@@ -421,7 +421,7 @@ def test_validate_backtest_data_warn(default_conf, mocker, caplog, testdatadir)
default_conf.update({'strategy': 'DefaultStrategy'})
strategy = StrategyResolver.load_strategy(default_conf)
data = strategy.tickerdata_to_dataframe(
data = strategy.ohlcvdata_to_dataframe(
load_data(
datadir=testdatadir,
timeframe='1m',
@@ -446,7 +446,7 @@ def test_validate_backtest_data(default_conf, mocker, caplog, testdatadir) -> No
strategy = StrategyResolver.load_strategy(default_conf)
timerange = TimeRange('index', 'index', 200, 250)
data = strategy.tickerdata_to_dataframe(
data = strategy.ohlcvdata_to_dataframe(
load_data(
datadir=testdatadir,
timeframe='5m',
@@ -547,6 +547,17 @@ def test_download_trades_history(trades_history, mocker, default_conf, testdatad
assert log_has("New Amount of trades: 5", caplog)
assert file1.is_file()
ght_mock.reset_mock()
since_time = int(trades_history[-3][0] // 1000)
since_time2 = int(trades_history[-1][0] // 1000)
timerange = TimeRange('date', None, since_time, 0)
assert _download_trades_history(data_handler=data_handler, exchange=exchange,
pair='ETH/BTC', timerange=timerange)
assert ght_mock.call_count == 1
# Check this in seconds - since we had to convert to seconds above too.
assert int(ght_mock.call_args_list[0][1]['since'] // 1000) == since_time2 - 5
# clean files freshly downloaded
_clean_test_file(file1)
@@ -601,7 +612,7 @@ def test_jsondatahandler_ohlcv_get_pairs(testdatadir):
def test_jsondatahandler_trades_get_pairs(testdatadir):
pairs = JsonGzDataHandler.trades_get_pairs(testdatadir)
# Convert to set to avoid failures due to sorting
assert set(pairs) == {'XRP/ETH'}
assert set(pairs) == {'XRP/ETH', 'XRP/OLD'}
def test_jsondatahandler_ohlcv_purge(mocker, testdatadir):
@@ -614,6 +625,17 @@ def test_jsondatahandler_ohlcv_purge(mocker, testdatadir):
assert dh.ohlcv_purge('UNITTEST/NONEXIST', '5m')
def test_jsondatahandler_trades_load(mocker, testdatadir, caplog):
dh = JsonGzDataHandler(testdatadir)
logmsg = "Old trades format detected - converting"
dh.trades_load('XRP/ETH')
assert not log_has(logmsg, caplog)
# Test conversation is happening
dh.trades_load('XRP/OLD')
assert log_has(logmsg, caplog)
def test_jsondatahandler_trades_purge(mocker, testdatadir):
mocker.patch.object(Path, "exists", MagicMock(return_value=False))
mocker.patch.object(Path, "unlink", MagicMock())

View File

@@ -11,7 +11,7 @@ import pytest
from pandas import DataFrame, to_datetime
from freqtrade.exceptions import OperationalException
from freqtrade.data.converter import parse_ticker_dataframe
from freqtrade.data.converter import ohlcv_to_dataframe
from freqtrade.edge import Edge, PairInfo
from freqtrade.strategy.interface import SellType
from tests.conftest import get_patched_freqtradebot, log_has
@@ -26,7 +26,7 @@ from tests.optimize import (BTContainer, BTrade, _build_backtest_dataframe,
# 5) Stoploss and sell are hit. should sell on stoploss
####################################################################
ticker_start_time = arrow.get(2018, 10, 3)
tests_start_time = arrow.get(2018, 10, 3)
ticker_interval_in_minute = 60
_ohlc = {'date': 0, 'buy': 1, 'open': 2, 'high': 3, 'low': 4, 'close': 5, 'sell': 6, 'volume': 7}
@@ -43,10 +43,10 @@ def _validate_ohlc(buy_ohlc_sell_matrice):
def _build_dataframe(buy_ohlc_sell_matrice):
_validate_ohlc(buy_ohlc_sell_matrice)
tickers = []
data = []
for ohlc in buy_ohlc_sell_matrice:
ticker = {
'date': ticker_start_time.shift(
d = {
'date': tests_start_time.shift(
minutes=(
ohlc[0] *
ticker_interval_in_minute)).timestamp *
@@ -57,9 +57,9 @@ def _build_dataframe(buy_ohlc_sell_matrice):
'low': ohlc[4],
'close': ohlc[5],
'sell': ohlc[6]}
tickers.append(ticker)
data.append(d)
frame = DataFrame(tickers)
frame = DataFrame(data)
frame['date'] = to_datetime(frame['date'],
unit='ms',
utc=True,
@@ -69,7 +69,7 @@ def _build_dataframe(buy_ohlc_sell_matrice):
def _time_on_candle(number):
return np.datetime64(ticker_start_time.shift(
return np.datetime64(tests_start_time.shift(
minutes=(number * ticker_interval_in_minute)).timestamp * 1000, 'ms')
@@ -163,8 +163,8 @@ def test_edge_results(edge_conf, mocker, caplog, data) -> None:
for c, trade in enumerate(data.trades):
res = results.iloc[c]
assert res.exit_type == trade.sell_reason
assert res.open_time == np.datetime64(_get_frame_time_from_offset(trade.open_tick))
assert res.close_time == np.datetime64(_get_frame_time_from_offset(trade.close_tick))
assert res.open_time == _get_frame_time_from_offset(trade.open_tick).replace(tzinfo=None)
assert res.close_time == _get_frame_time_from_offset(trade.close_tick).replace(tzinfo=None)
def test_adjust(mocker, edge_conf):
@@ -262,7 +262,7 @@ def mocked_load_data(datadir, pairs=[], timeframe='0m',
NEOBTC = [
[
ticker_start_time.shift(minutes=(x * ticker_interval_in_minute)).timestamp * 1000,
tests_start_time.shift(minutes=(x * ticker_interval_in_minute)).timestamp * 1000,
math.sin(x * hz) / 1000 + base,
math.sin(x * hz) / 1000 + base + 0.0001,
math.sin(x * hz) / 1000 + base - 0.0001,
@@ -274,7 +274,7 @@ def mocked_load_data(datadir, pairs=[], timeframe='0m',
base = 0.002
LTCBTC = [
[
ticker_start_time.shift(minutes=(x * ticker_interval_in_minute)).timestamp * 1000,
tests_start_time.shift(minutes=(x * ticker_interval_in_minute)).timestamp * 1000,
math.sin(x * hz) / 1000 + base,
math.sin(x * hz) / 1000 + base + 0.0001,
math.sin(x * hz) / 1000 + base - 0.0001,
@@ -282,16 +282,18 @@ def mocked_load_data(datadir, pairs=[], timeframe='0m',
123.45
] for x in range(0, 500)]
pairdata = {'NEO/BTC': parse_ticker_dataframe(NEOBTC, '1h', pair="NEO/BTC", fill_missing=True),
'LTC/BTC': parse_ticker_dataframe(LTCBTC, '1h', pair="LTC/BTC", fill_missing=True)}
pairdata = {'NEO/BTC': ohlcv_to_dataframe(NEOBTC, '1h', pair="NEO/BTC",
fill_missing=True),
'LTC/BTC': ohlcv_to_dataframe(LTCBTC, '1h', pair="LTC/BTC",
fill_missing=True)}
return pairdata
def test_edge_process_downloaded_data(mocker, edge_conf):
freqtrade = get_patched_freqtradebot(mocker, edge_conf)
mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.001))
mocker.patch('freqtrade.data.history.refresh_data', MagicMock())
mocker.patch('freqtrade.data.history.load_data', mocked_load_data)
mocker.patch('freqtrade.edge.edge_positioning.refresh_data', MagicMock())
mocker.patch('freqtrade.edge.edge_positioning.load_data', mocked_load_data)
edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy)
assert edge.calculate()
@@ -302,8 +304,8 @@ def test_edge_process_downloaded_data(mocker, edge_conf):
def test_edge_process_no_data(mocker, edge_conf, caplog):
freqtrade = get_patched_freqtradebot(mocker, edge_conf)
mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.001))
mocker.patch('freqtrade.data.history.refresh_data', MagicMock())
mocker.patch('freqtrade.data.history.load_data', MagicMock(return_value={}))
mocker.patch('freqtrade.edge.edge_positioning.refresh_data', MagicMock())
mocker.patch('freqtrade.edge.edge_positioning.load_data', MagicMock(return_value={}))
edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy)
assert not edge.calculate()
@@ -315,8 +317,8 @@ def test_edge_process_no_data(mocker, edge_conf, caplog):
def test_edge_process_no_trades(mocker, edge_conf, caplog):
freqtrade = get_patched_freqtradebot(mocker, edge_conf)
mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.001))
mocker.patch('freqtrade.data.history.refresh_data', MagicMock())
mocker.patch('freqtrade.data.history.load_data', mocked_load_data)
mocker.patch('freqtrade.edge.edge_positioning.refresh_data', MagicMock())
mocker.patch('freqtrade.edge.edge_positioning.load_data', mocked_load_data)
# Return empty
mocker.patch('freqtrade.edge.Edge._find_trades_for_stoploss_range', MagicMock(return_value=[]))
edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy)
@@ -333,12 +335,16 @@ def test_edge_init_error(mocker, edge_conf,):
get_patched_freqtradebot(mocker, edge_conf)
def test_process_expectancy(mocker, edge_conf):
@pytest.mark.parametrize("fee,risk_reward_ratio,expectancy", [
(0.0005, 306.5384615384, 101.5128205128),
(0.001, 152.6923076923, 50.2307692308),
])
def test_process_expectancy(mocker, edge_conf, fee, risk_reward_ratio, expectancy):
edge_conf['edge']['min_trade_number'] = 2
freqtrade = get_patched_freqtradebot(mocker, edge_conf)
def get_fee(*args, **kwargs):
return 0.001
return fee
freqtrade.exchange.get_fee = get_fee
edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy)
@@ -392,9 +398,9 @@ def test_process_expectancy(mocker, edge_conf):
assert 'TEST/BTC' in final
assert final['TEST/BTC'].stoploss == -0.9
assert round(final['TEST/BTC'].winrate, 10) == 0.3333333333
assert round(final['TEST/BTC'].risk_reward_ratio, 10) == 306.5384615384
assert round(final['TEST/BTC'].risk_reward_ratio, 10) == risk_reward_ratio
assert round(final['TEST/BTC'].required_risk_reward, 10) == 2.0
assert round(final['TEST/BTC'].expectancy, 10) == 101.5128205128
assert round(final['TEST/BTC'].expectancy, 10) == expectancy
# Pop last item so no trade is profitable
trades.pop()

View File

@@ -9,7 +9,12 @@ from freqtrade.exceptions import (DependencyException, InvalidOrderException,
from tests.conftest import get_patched_exchange
def test_stoploss_order_binance(default_conf, mocker):
@pytest.mark.parametrize('limitratio,expected', [
(None, 220 * 0.99),
(0.99, 220 * 0.99),
(0.98, 220 * 0.98),
])
def test_stoploss_order_binance(default_conf, mocker, limitratio, expected):
api_mock = MagicMock()
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
order_type = 'stop_loss_limit'
@@ -20,7 +25,6 @@ def test_stoploss_order_binance(default_conf, mocker):
'foo': 'bar'
}
})
default_conf['dry_run'] = False
mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y)
@@ -32,8 +36,8 @@ def test_stoploss_order_binance(default_conf, mocker):
order_types={'stoploss_on_exchange_limit_ratio': 1.05})
api_mock.create_order.reset_mock()
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
order_types = {} if limitratio is None else {'stoploss_on_exchange_limit_ratio': limitratio}
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types=order_types)
assert 'id' in order
assert 'info' in order
@@ -42,7 +46,8 @@ def test_stoploss_order_binance(default_conf, mocker):
assert api_mock.create_order.call_args_list[0][1]['type'] == order_type
assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell'
assert api_mock.create_order.call_args_list[0][1]['amount'] == 1
assert api_mock.create_order.call_args_list[0][1]['price'] == 220
# Price should be 1% below stopprice
assert api_mock.create_order.call_args_list[0][1]['price'] == expected
assert api_mock.create_order.call_args_list[0][1]['params'] == {'stopPrice': 220}
# test exception handling

View File

@@ -253,6 +253,32 @@ def test_price_to_precision(default_conf, mocker, price, precision_mode, precisi
assert pytest.approx(exchange.price_to_precision(pair, price)) == expected
@pytest.mark.parametrize("price,precision_mode,precision,expected", [
(2.34559, 2, 4, 0.0001),
(2.34559, 2, 5, 0.00001),
(2.34559, 2, 3, 0.001),
(2.9999, 2, 3, 0.001),
(200.0511, 2, 3, 0.001),
# Tests for Tick_size
(2.34559, 4, 0.0001, 0.0001),
(2.34559, 4, 0.00001, 0.00001),
(2.34559, 4, 0.0025, 0.0025),
(2.9909, 4, 0.0025, 0.0025),
(234.43, 4, 0.5, 0.5),
(234.43, 4, 0.0025, 0.0025),
(234.43, 4, 0.00013, 0.00013),
])
def test_price_get_one_pip(default_conf, mocker, price, precision_mode, precision, expected):
markets = PropertyMock(return_value={'ETH/BTC': {'precision': {'price': precision}}})
exchange = get_patched_exchange(mocker, default_conf, id="binance")
mocker.patch('freqtrade.exchange.Exchange.markets', markets)
mocker.patch('freqtrade.exchange.Exchange.precisionMode',
PropertyMock(return_value=precision_mode))
pair = 'ETH/BTC'
assert pytest.approx(exchange.price_get_one_pip(pair, price)) == expected
def test_set_sandbox(default_conf, mocker):
"""
Test working scenario
@@ -491,9 +517,9 @@ def test_validate_pairs_restricted(default_conf, mocker, caplog):
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
Exchange(default_conf)
assert log_has(f"Pair XRP/BTC is restricted for some users on this exchange."
f"Please check if you are impacted by this restriction "
f"on the exchange and eventually remove XRP/BTC from your whitelist.", caplog)
assert log_has("Pair XRP/BTC is restricted for some users on this exchange."
"Please check if you are impacted by this restriction "
"on the exchange and eventually remove XRP/BTC from your whitelist.", caplog)
def test_validate_pairs_stakecompatibility(default_conf, mocker, caplog):
@@ -581,7 +607,7 @@ def test_validate_timeframes_failed(default_conf, mocker):
mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
with pytest.raises(OperationalException,
match=r"Invalid ticker interval '3m'. This exchange supports.*"):
match=r"Invalid timeframe '3m'. This exchange supports.*"):
Exchange(default_conf)
default_conf["ticker_interval"] = "15s"
@@ -1211,7 +1237,7 @@ def test_fetch_ticker(default_conf, mocker, exchange_name):
@pytest.mark.parametrize("exchange_name", EXCHANGES)
def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name):
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
tick = [
ohlcv = [
[
arrow.utcnow().timestamp * 1000, # unix timestamp ms
1, # open
@@ -1224,7 +1250,7 @@ def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name):
pair = 'ETH/BTC'
async def mock_candle_hist(pair, timeframe, since_ms):
return pair, timeframe, tick
return pair, timeframe, ohlcv
exchange._async_get_candle_history = Mock(wraps=mock_candle_hist)
# one_call calculation * 1.8 should do 2 calls
@@ -1232,12 +1258,12 @@ def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name):
ret = exchange.get_historic_ohlcv(pair, "5m", int((arrow.utcnow().timestamp - since) * 1000))
assert exchange._async_get_candle_history.call_count == 2
# Returns twice the above tick
# Returns twice the above OHLCV data
assert len(ret) == 2
def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None:
tick = [
ohlcv = [
[
(arrow.utcnow().timestamp - 1) * 1000, # unix timestamp ms
1, # open
@@ -1258,14 +1284,14 @@ def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None:
caplog.set_level(logging.DEBUG)
exchange = get_patched_exchange(mocker, default_conf)
exchange._api_async.fetch_ohlcv = get_mock_coro(tick)
exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv)
pairs = [('IOTA/ETH', '5m'), ('XRP/ETH', '5m')]
# empty dicts
assert not exchange._klines
exchange.refresh_latest_ohlcv(pairs)
assert log_has(f'Refreshing ohlcv data for {len(pairs)} pairs', caplog)
assert log_has(f'Refreshing candle (OHLCV) data for {len(pairs)} pairs', caplog)
assert exchange._klines
assert exchange._api_async.fetch_ohlcv.call_count == 2
for pair in pairs:
@@ -1283,14 +1309,15 @@ def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None:
exchange.refresh_latest_ohlcv([('IOTA/ETH', '5m'), ('XRP/ETH', '5m')])
assert exchange._api_async.fetch_ohlcv.call_count == 2
assert log_has(f"Using cached ohlcv data for pair {pairs[0][0]}, timeframe {pairs[0][1]} ...",
assert log_has(f"Using cached candle (OHLCV) data for pair {pairs[0][0]}, "
f"timeframe {pairs[0][1]} ...",
caplog)
@pytest.mark.asyncio
@pytest.mark.parametrize("exchange_name", EXCHANGES)
async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_name):
tick = [
ohlcv = [
[
arrow.utcnow().timestamp * 1000, # unix timestamp ms
1, # open
@@ -1304,7 +1331,7 @@ async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_
caplog.set_level(logging.DEBUG)
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
# Monkey-patch async function
exchange._api_async.fetch_ohlcv = get_mock_coro(tick)
exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv)
pair = 'ETH/BTC'
res = await exchange._async_get_candle_history(pair, "5m")
@@ -1312,9 +1339,9 @@ async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_
assert len(res) == 3
assert res[0] == pair
assert res[1] == "5m"
assert res[2] == tick
assert res[2] == ohlcv
assert exchange._api_async.fetch_ohlcv.call_count == 1
assert not log_has(f"Using cached ohlcv data for {pair} ...", caplog)
assert not log_has(f"Using cached candle (OHLCV) data for {pair} ...", caplog)
# exchange = Exchange(default_conf)
await async_ccxt_exception(mocker, default_conf, MagicMock(),
@@ -1322,14 +1349,15 @@ async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_
pair='ABCD/BTC', timeframe=default_conf['ticker_interval'])
api_mock = MagicMock()
with pytest.raises(OperationalException, match=r'Could not fetch ticker data*'):
with pytest.raises(OperationalException,
match=r'Could not fetch historical candle \(OHLCV\) data.*'):
api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.BaseError("Unknown error"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
await exchange._async_get_candle_history(pair, "5m",
(arrow.utcnow().timestamp - 2000) * 1000)
with pytest.raises(OperationalException, match=r'Exchange.* does not support fetching '
r'historical candlestick data\..*'):
r'historical candle \(OHLCV\) data\..*'):
api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.NotSupported("Not supported"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
await exchange._async_get_candle_history(pair, "5m",
@@ -1339,7 +1367,7 @@ async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_
@pytest.mark.asyncio
async def test__async_get_candle_history_empty(default_conf, mocker, caplog):
""" Test empty exchange result """
tick = []
ohlcv = []
caplog.set_level(logging.DEBUG)
exchange = get_patched_exchange(mocker, default_conf)
@@ -1353,7 +1381,7 @@ async def test__async_get_candle_history_empty(default_conf, mocker, caplog):
assert len(res) == 3
assert res[0] == pair
assert res[1] == "5m"
assert res[2] == tick
assert res[2] == ohlcv
assert exchange._api_async.fetch_ohlcv.call_count == 1
@@ -1431,8 +1459,8 @@ async def test___async_get_candle_history_sort(default_conf, mocker, exchange_na
return sorted(data, key=key)
# GDAX use-case (real data from GDAX)
# This ticker history is ordered DESC (newest first, oldest last)
tick = [
# This OHLCV data is ordered DESC (newest first, oldest last)
ohlcv = [
[1527833100000, 0.07666, 0.07671, 0.07666, 0.07668, 16.65244264],
[1527832800000, 0.07662, 0.07666, 0.07662, 0.07666, 1.30051526],
[1527832500000, 0.07656, 0.07661, 0.07656, 0.07661, 12.034778840000001],
@@ -1445,31 +1473,31 @@ async def test___async_get_candle_history_sort(default_conf, mocker, exchange_na
[1527830400000, 0.07649, 0.07651, 0.07649, 0.07651, 2.5734867]
]
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
exchange._api_async.fetch_ohlcv = get_mock_coro(tick)
exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv)
sort_mock = mocker.patch('freqtrade.exchange.exchange.sorted', MagicMock(side_effect=sort_data))
# Test the ticker history sort
# Test the OHLCV data sort
res = await exchange._async_get_candle_history('ETH/BTC', default_conf['ticker_interval'])
assert res[0] == 'ETH/BTC'
ticks = res[2]
res_ohlcv = res[2]
assert sort_mock.call_count == 1
assert ticks[0][0] == 1527830400000
assert ticks[0][1] == 0.07649
assert ticks[0][2] == 0.07651
assert ticks[0][3] == 0.07649
assert ticks[0][4] == 0.07651
assert ticks[0][5] == 2.5734867
assert res_ohlcv[0][0] == 1527830400000
assert res_ohlcv[0][1] == 0.07649
assert res_ohlcv[0][2] == 0.07651
assert res_ohlcv[0][3] == 0.07649
assert res_ohlcv[0][4] == 0.07651
assert res_ohlcv[0][5] == 2.5734867
assert ticks[9][0] == 1527833100000
assert ticks[9][1] == 0.07666
assert ticks[9][2] == 0.07671
assert ticks[9][3] == 0.07666
assert ticks[9][4] == 0.07668
assert ticks[9][5] == 16.65244264
assert res_ohlcv[9][0] == 1527833100000
assert res_ohlcv[9][1] == 0.07666
assert res_ohlcv[9][2] == 0.07671
assert res_ohlcv[9][3] == 0.07666
assert res_ohlcv[9][4] == 0.07668
assert res_ohlcv[9][5] == 16.65244264
# Bittrex use-case (real data from Bittrex)
# This ticker history is ordered ASC (oldest first, newest last)
tick = [
# This OHLCV data is ordered ASC (oldest first, newest last)
ohlcv = [
[1527827700000, 0.07659999, 0.0766, 0.07627, 0.07657998, 1.85216924],
[1527828000000, 0.07657995, 0.07657995, 0.0763, 0.0763, 26.04051037],
[1527828300000, 0.0763, 0.07659998, 0.0763, 0.0764, 10.36434124],
@@ -1481,46 +1509,46 @@ async def test___async_get_candle_history_sort(default_conf, mocker, exchange_na
[1527830100000, 0.076695, 0.07671, 0.07624171, 0.07671, 1.80689244],
[1527830400000, 0.07671, 0.07674399, 0.07629216, 0.07655213, 2.31452783]
]
exchange._api_async.fetch_ohlcv = get_mock_coro(tick)
exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv)
# Reset sort mock
sort_mock = mocker.patch('freqtrade.exchange.sorted', MagicMock(side_effect=sort_data))
# Test the ticker history sort
# Test the OHLCV data sort
res = await exchange._async_get_candle_history('ETH/BTC', default_conf['ticker_interval'])
assert res[0] == 'ETH/BTC'
assert res[1] == default_conf['ticker_interval']
ticks = res[2]
res_ohlcv = res[2]
# Sorted not called again - data is already in order
assert sort_mock.call_count == 0
assert ticks[0][0] == 1527827700000
assert ticks[0][1] == 0.07659999
assert ticks[0][2] == 0.0766
assert ticks[0][3] == 0.07627
assert ticks[0][4] == 0.07657998
assert ticks[0][5] == 1.85216924
assert res_ohlcv[0][0] == 1527827700000
assert res_ohlcv[0][1] == 0.07659999
assert res_ohlcv[0][2] == 0.0766
assert res_ohlcv[0][3] == 0.07627
assert res_ohlcv[0][4] == 0.07657998
assert res_ohlcv[0][5] == 1.85216924
assert ticks[9][0] == 1527830400000
assert ticks[9][1] == 0.07671
assert ticks[9][2] == 0.07674399
assert ticks[9][3] == 0.07629216
assert ticks[9][4] == 0.07655213
assert ticks[9][5] == 2.31452783
assert res_ohlcv[9][0] == 1527830400000
assert res_ohlcv[9][1] == 0.07671
assert res_ohlcv[9][2] == 0.07674399
assert res_ohlcv[9][3] == 0.07629216
assert res_ohlcv[9][4] == 0.07655213
assert res_ohlcv[9][5] == 2.31452783
@pytest.mark.asyncio
@pytest.mark.parametrize("exchange_name", EXCHANGES)
async def test__async_fetch_trades(default_conf, mocker, caplog, exchange_name,
trades_history):
fetch_trades_result):
caplog.set_level(logging.DEBUG)
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
# Monkey-patch async function
exchange._api_async.fetch_trades = get_mock_coro(trades_history)
exchange._api_async.fetch_trades = get_mock_coro(fetch_trades_result)
pair = 'ETH/BTC'
res = await exchange._async_fetch_trades(pair, since=None, params=None)
assert type(res) is list
assert isinstance(res[0], dict)
assert isinstance(res[1], dict)
assert isinstance(res[0], list)
assert isinstance(res[1], list)
assert exchange._api_async.fetch_trades.call_count == 1
assert exchange._api_async.fetch_trades.call_args[0][0] == pair
@@ -1566,7 +1594,7 @@ async def test__async_get_trade_history_id(default_conf, mocker, caplog, exchang
if 'since' in kwargs:
# Return first 3
return trades_history[:-2]
elif kwargs.get('params', {}).get(pagination_arg) == trades_history[-3]['id']:
elif kwargs.get('params', {}).get(pagination_arg) == trades_history[-3][1]:
# Return 2
return trades_history[-3:-1]
else:
@@ -1576,8 +1604,8 @@ async def test__async_get_trade_history_id(default_conf, mocker, caplog, exchang
exchange._async_fetch_trades = MagicMock(side_effect=mock_get_trade_hist)
pair = 'ETH/BTC'
ret = await exchange._async_get_trade_history_id(pair, since=trades_history[0]["timestamp"],
until=trades_history[-1]["timestamp"]-1)
ret = await exchange._async_get_trade_history_id(pair, since=trades_history[0][0],
until=trades_history[-1][0]-1)
assert type(ret) is tuple
assert ret[0] == pair
assert type(ret[1]) is list
@@ -1586,7 +1614,7 @@ async def test__async_get_trade_history_id(default_conf, mocker, caplog, exchang
fetch_trades_cal = exchange._async_fetch_trades.call_args_list
# first call (using since, not fromId)
assert fetch_trades_cal[0][0][0] == pair
assert fetch_trades_cal[0][1]['since'] == trades_history[0]["timestamp"]
assert fetch_trades_cal[0][1]['since'] == trades_history[0][0]
# 2nd call
assert fetch_trades_cal[1][0][0] == pair
@@ -1602,7 +1630,7 @@ async def test__async_get_trade_history_time(default_conf, mocker, caplog, excha
caplog.set_level(logging.DEBUG)
async def mock_get_trade_hist(pair, *args, **kwargs):
if kwargs['since'] == trades_history[0]["timestamp"]:
if kwargs['since'] == trades_history[0][0]:
return trades_history[:-1]
else:
return trades_history[-1:]
@@ -1612,8 +1640,8 @@ async def test__async_get_trade_history_time(default_conf, mocker, caplog, excha
# Monkey-patch async function
exchange._async_fetch_trades = MagicMock(side_effect=mock_get_trade_hist)
pair = 'ETH/BTC'
ret = await exchange._async_get_trade_history_time(pair, since=trades_history[0]["timestamp"],
until=trades_history[-1]["timestamp"]-1)
ret = await exchange._async_get_trade_history_time(pair, since=trades_history[0][0],
until=trades_history[-1][0]-1)
assert type(ret) is tuple
assert ret[0] == pair
assert type(ret[1]) is list
@@ -1622,11 +1650,11 @@ async def test__async_get_trade_history_time(default_conf, mocker, caplog, excha
fetch_trades_cal = exchange._async_fetch_trades.call_args_list
# first call (using since, not fromId)
assert fetch_trades_cal[0][0][0] == pair
assert fetch_trades_cal[0][1]['since'] == trades_history[0]["timestamp"]
assert fetch_trades_cal[0][1]['since'] == trades_history[0][0]
# 2nd call
assert fetch_trades_cal[1][0][0] == pair
assert fetch_trades_cal[0][1]['since'] == trades_history[0]["timestamp"]
assert fetch_trades_cal[0][1]['since'] == trades_history[0][0]
assert log_has_re(r"Stopping because until was reached.*", caplog)
@@ -1638,7 +1666,7 @@ async def test__async_get_trade_history_time_empty(default_conf, mocker, caplog,
caplog.set_level(logging.DEBUG)
async def mock_get_trade_hist(pair, *args, **kwargs):
if kwargs['since'] == trades_history[0]["timestamp"]:
if kwargs['since'] == trades_history[0][0]:
return trades_history[:-1]
else:
return []
@@ -1648,8 +1676,8 @@ async def test__async_get_trade_history_time_empty(default_conf, mocker, caplog,
# Monkey-patch async function
exchange._async_fetch_trades = MagicMock(side_effect=mock_get_trade_hist)
pair = 'ETH/BTC'
ret = await exchange._async_get_trade_history_time(pair, since=trades_history[0]["timestamp"],
until=trades_history[-1]["timestamp"]-1)
ret = await exchange._async_get_trade_history_time(pair, since=trades_history[0][0],
until=trades_history[-1][0]-1)
assert type(ret) is tuple
assert ret[0] == pair
assert type(ret[1]) is list
@@ -1658,7 +1686,7 @@ async def test__async_get_trade_history_time_empty(default_conf, mocker, caplog,
fetch_trades_cal = exchange._async_fetch_trades.call_args_list
# first call (using since, not fromId)
assert fetch_trades_cal[0][0][0] == pair
assert fetch_trades_cal[0][1]['since'] == trades_history[0]["timestamp"]
assert fetch_trades_cal[0][1]['since'] == trades_history[0][0]
@pytest.mark.parametrize("exchange_name", EXCHANGES)
@@ -1670,8 +1698,8 @@ def test_get_historic_trades(default_conf, mocker, caplog, exchange_name, trades
exchange._async_get_trade_history_id = get_mock_coro((pair, trades_history))
exchange._async_get_trade_history_time = get_mock_coro((pair, trades_history))
ret = exchange.get_historic_trades(pair, since=trades_history[0]["timestamp"],
until=trades_history[-1]["timestamp"])
ret = exchange.get_historic_trades(pair, since=trades_history[0][0],
until=trades_history[-1][0])
# Depending on the exchange, one or the other method should be called
assert sum([exchange._async_get_trade_history_id.call_count,
@@ -1692,15 +1720,77 @@ def test_get_historic_trades_notsupported(default_conf, mocker, caplog, exchange
with pytest.raises(OperationalException,
match="This exchange does not suport downloading Trades."):
exchange.get_historic_trades(pair, since=trades_history[0]["timestamp"],
until=trades_history[-1]["timestamp"])
exchange.get_historic_trades(pair, since=trades_history[0][0],
until=trades_history[-1][0])
@pytest.mark.parametrize("exchange_name", EXCHANGES)
def test_cancel_order_dry_run(default_conf, mocker, exchange_name):
default_conf['dry_run'] = True
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
assert exchange.cancel_order(order_id='123', pair='TKN/BTC') is None
assert exchange.cancel_order(order_id='123', pair='TKN/BTC') == {}
@pytest.mark.parametrize("exchange_name", EXCHANGES)
@pytest.mark.parametrize("order,result", [
({'status': 'closed', 'filled': 10}, False),
({'status': 'closed', 'filled': 0.0}, True),
({'status': 'canceled', 'filled': 0.0}, True),
({'status': 'canceled', 'filled': 10.0}, False),
({'status': 'unknown', 'filled': 10.0}, False),
({'result': 'testest123'}, False),
])
def test_check_order_canceled_empty(mocker, default_conf, exchange_name, order, result):
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
assert exchange.check_order_canceled_empty(order) == result
@pytest.mark.parametrize("exchange_name", EXCHANGES)
@pytest.mark.parametrize("order,result", [
({'status': 'closed', 'amount': 10, 'fee': {}}, True),
({'status': 'closed', 'amount': 0.0, 'fee': {}}, True),
({'status': 'canceled', 'amount': 0.0, 'fee': {}}, True),
({'status': 'canceled', 'amount': 10.0}, False),
({'amount': 10.0, 'fee': {}}, False),
({'result': 'testest123'}, False),
('hello_world', False),
])
def test_is_cancel_order_result_suitable(mocker, default_conf, exchange_name, order, result):
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
assert exchange.is_cancel_order_result_suitable(order) == result
@pytest.mark.parametrize("exchange_name", EXCHANGES)
@pytest.mark.parametrize("corder,call_corder,call_forder", [
({'status': 'closed', 'amount': 10, 'fee': {}}, 1, 0),
({'amount': 10, 'fee': {}}, 1, 1),
])
def test_cancel_order_with_result(default_conf, mocker, exchange_name, corder,
call_corder, call_forder):
default_conf['dry_run'] = False
api_mock = MagicMock()
api_mock.cancel_order = MagicMock(return_value=corder)
api_mock.fetch_order = MagicMock(return_value={})
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
res = exchange.cancel_order_with_result('1234', 'ETH/BTC', 1234)
assert isinstance(res, dict)
assert api_mock.cancel_order.call_count == call_corder
assert api_mock.fetch_order.call_count == call_forder
@pytest.mark.parametrize("exchange_name", EXCHANGES)
def test_cancel_order_with_result_error(default_conf, mocker, exchange_name, caplog):
default_conf['dry_run'] = False
api_mock = MagicMock()
api_mock.cancel_order = MagicMock(side_effect=ccxt.InvalidOrder("Did not find order"))
api_mock.fetch_order = MagicMock(side_effect=ccxt.InvalidOrder("Did not find order"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
res = exchange.cancel_order_with_result('1234', 'ETH/BTC', 1541)
assert isinstance(res, dict)
assert log_has("Could not cancel order 1234.", caplog)
assert log_has("Could not fetch cancelled order 1234.", caplog)
assert res['amount'] == 1541
# Ensure that if not dry_run, we should call API
@@ -2055,3 +2145,58 @@ def test_symbol_is_pair(market_symbol, base_currency, quote_currency, expected_r
])
def test_market_is_active(market, expected_result) -> None:
assert market_is_active(market) == expected_result
@pytest.mark.parametrize("order,expected", [
([{'fee'}], False),
({'fee': None}, False),
({'fee': {'currency': 'ETH/BTC'}}, False),
({'fee': {'currency': 'ETH/BTC', 'cost': None}}, False),
({'fee': {'currency': 'ETH/BTC', 'cost': 0.01}}, True),
])
def test_order_has_fee(order, expected) -> None:
assert Exchange.order_has_fee(order) == expected
@pytest.mark.parametrize("order,expected", [
({'symbol': 'ETH/BTC', 'fee': {'currency': 'ETH', 'cost': 0.43}},
(0.43, 'ETH', 0.01)),
({'symbol': 'ETH/USDT', 'fee': {'currency': 'USDT', 'cost': 0.01}},
(0.01, 'USDT', 0.01)),
({'symbol': 'BTC/USDT', 'fee': {'currency': 'USDT', 'cost': 0.34, 'rate': 0.01}},
(0.34, 'USDT', 0.01)),
])
def test_extract_cost_curr_rate(mocker, default_conf, order, expected) -> None:
mocker.patch('freqtrade.exchange.Exchange.calculate_fee_rate', MagicMock(return_value=0.01))
ex = get_patched_exchange(mocker, default_conf)
assert ex.extract_cost_curr_rate(order) == expected
@pytest.mark.parametrize("order,expected", [
# Using base-currency
({'symbol': 'ETH/BTC', 'amount': 0.04, 'cost': 0.05,
'fee': {'currency': 'ETH', 'cost': 0.004, 'rate': None}}, 0.1),
({'symbol': 'ETH/BTC', 'amount': 0.05, 'cost': 0.05,
'fee': {'currency': 'ETH', 'cost': 0.004, 'rate': None}}, 0.08),
# Using quote currency
({'symbol': 'ETH/BTC', 'amount': 0.04, 'cost': 0.05,
'fee': {'currency': 'BTC', 'cost': 0.005}}, 0.1),
({'symbol': 'ETH/BTC', 'amount': 0.04, 'cost': 0.05,
'fee': {'currency': 'BTC', 'cost': 0.002, 'rate': None}}, 0.04),
# Using foreign currency
({'symbol': 'ETH/BTC', 'amount': 0.04, 'cost': 0.05,
'fee': {'currency': 'NEO', 'cost': 0.0012}}, 0.001944),
({'symbol': 'ETH/BTC', 'amount': 2.21, 'cost': 0.02992561,
'fee': {'currency': 'NEO', 'cost': 0.00027452}}, 0.00074305),
# TODO: More tests here!
# Rate included in return - return as is
({'symbol': 'ETH/BTC', 'amount': 0.04, 'cost': 0.05,
'fee': {'currency': 'USDT', 'cost': 0.34, 'rate': 0.01}}, 0.01),
({'symbol': 'ETH/BTC', 'amount': 0.04, 'cost': 0.05,
'fee': {'currency': 'USDT', 'cost': 0.34, 'rate': 0.005}}, 0.005),
])
def test_calculate_fee_rate(mocker, default_conf, order, expected) -> None:
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', return_value={'last': 0.081})
ex = get_patched_exchange(mocker, default_conf)
assert ex.calculate_fee_rate(order) == expected

View File

@@ -6,7 +6,7 @@ from pandas import DataFrame
from freqtrade.exchange import timeframe_to_minutes
from freqtrade.strategy.interface import SellType
ticker_start_time = arrow.get(2018, 10, 3)
tests_start_time = arrow.get(2018, 10, 3)
tests_timeframe = '1h'
@@ -36,14 +36,14 @@ class BTContainer(NamedTuple):
def _get_frame_time_from_offset(offset):
return ticker_start_time.shift(minutes=(offset * timeframe_to_minutes(tests_timeframe))
).datetime
minutes = offset * timeframe_to_minutes(tests_timeframe)
return tests_start_time.shift(minutes=minutes).datetime
def _build_backtest_dataframe(ticker_with_signals):
def _build_backtest_dataframe(data):
columns = ['date', 'open', 'high', 'low', 'close', 'volume', 'buy', 'sell']
frame = DataFrame.from_records(ticker_with_signals, columns=columns)
frame = DataFrame.from_records(data, columns=columns)
frame['date'] = frame['date'].apply(_get_frame_time_from_offset)
# Ensure floats are in place
for column in ['open', 'high', 'low', 'close', 'volume']:

View File

@@ -2,7 +2,7 @@
import random
from pathlib import Path
from unittest.mock import MagicMock
from unittest.mock import MagicMock, PropertyMock
import numpy as np
import pandas as pd
@@ -10,8 +10,9 @@ import pytest
from arrow import Arrow
from freqtrade import constants
from freqtrade.commands.optimize_commands import (setup_optimize_configuration,
start_backtesting)
from freqtrade.configuration import TimeRange
from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_backtesting
from freqtrade.data import history
from freqtrade.data.btanalysis import evaluate_result_multi
from freqtrade.data.converter import clean_ohlcv_dataframe
@@ -84,7 +85,7 @@ def simple_backtest(config, contour, num_results, mocker, testdatadir) -> None:
backtesting = Backtesting(config)
data = load_data_test(contour, testdatadir)
processed = backtesting.strategy.tickerdata_to_dataframe(data)
processed = backtesting.strategy.ohlcvdata_to_dataframe(data)
min_date, max_date = get_timerange(processed)
assert isinstance(processed, dict)
results = backtesting.backtest(
@@ -105,7 +106,7 @@ def _make_backtest_conf(mocker, datadir, conf=None, pair='UNITTEST/BTC'):
data = trim_dictlist(data, -201)
patch_exchange(mocker)
backtesting = Backtesting(conf)
processed = backtesting.strategy.tickerdata_to_dataframe(data)
processed = backtesting.strategy.ohlcvdata_to_dataframe(data)
min_date, max_date = get_timerange(processed)
return {
'processed': processed,
@@ -224,6 +225,7 @@ def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) ->
assert 'export' in config
assert log_has('Parameter --export detected: {} ...'.format(config['export']), caplog)
assert 'exportfilename' in config
assert isinstance(config['exportfilename'], Path)
assert log_has('Storing backtest results to {} ...'.format(config['exportfilename']), caplog)
assert 'fee' in config
@@ -275,7 +277,7 @@ def test_backtesting_init(mocker, default_conf, order_types) -> None:
backtesting = Backtesting(default_conf)
assert backtesting.config == default_conf
assert backtesting.timeframe == '5m'
assert callable(backtesting.strategy.tickerdata_to_dataframe)
assert callable(backtesting.strategy.ohlcvdata_to_dataframe)
assert callable(backtesting.strategy.advise_buy)
assert callable(backtesting.strategy.advise_sell)
assert isinstance(backtesting.strategy.dp, DataProvider)
@@ -297,7 +299,7 @@ def test_backtesting_init_no_ticker_interval(mocker, default_conf, caplog) -> No
"or as cli argument `--ticker-interval 5m`", caplog)
def test_tickerdata_with_fee(default_conf, mocker, testdatadir) -> None:
def test_data_with_fee(default_conf, mocker, testdatadir) -> None:
patch_exchange(mocker)
default_conf['fee'] = 0.1234
@@ -307,21 +309,21 @@ def test_tickerdata_with_fee(default_conf, mocker, testdatadir) -> None:
assert fee_mock.call_count == 0
def test_tickerdata_to_dataframe_bt(default_conf, mocker, testdatadir) -> None:
def test_data_to_dataframe_bt(default_conf, mocker, testdatadir) -> None:
patch_exchange(mocker)
timerange = TimeRange.parse_timerange('1510694220-1510700340')
tickerlist = history.load_data(testdatadir, '1m', ['UNITTEST/BTC'], timerange=timerange,
fill_up_missing=True)
data = history.load_data(testdatadir, '1m', ['UNITTEST/BTC'], timerange=timerange,
fill_up_missing=True)
backtesting = Backtesting(default_conf)
data = backtesting.strategy.tickerdata_to_dataframe(tickerlist)
assert len(data['UNITTEST/BTC']) == 102
processed = backtesting.strategy.ohlcvdata_to_dataframe(data)
assert len(processed['UNITTEST/BTC']) == 102
# Load strategy to compare the result between Backtesting function and strategy are the same
default_conf.update({'strategy': 'DefaultStrategy'})
strategy = StrategyResolver.load_strategy(default_conf)
data2 = strategy.tickerdata_to_dataframe(tickerlist)
assert data['UNITTEST/BTC'].equals(data2['UNITTEST/BTC'])
processed2 = strategy.ohlcvdata_to_dataframe(data)
assert processed['UNITTEST/BTC'].equals(processed2['UNITTEST/BTC'])
def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None:
@@ -329,12 +331,12 @@ def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None:
return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59)
mocker.patch('freqtrade.data.history.get_timerange', get_timerange)
mocker.patch('freqtrade.exchange.Exchange.refresh_latest_ohlcv', MagicMock())
patch_exchange(mocker)
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', MagicMock())
mocker.patch('freqtrade.optimize.backtesting.generate_text_table', MagicMock(return_value=1))
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest')
mocker.patch('freqtrade.optimize.backtesting.show_backtest_results')
mocker.patch('freqtrade.pairlist.pairlistmanager.PairListManager.whitelist',
PropertyMock(return_value=['UNITTEST/BTC']))
default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
default_conf['ticker_interval'] = '1m'
default_conf['datadir'] = testdatadir
default_conf['export'] = None
@@ -360,12 +362,11 @@ def test_backtesting_start_no_data(default_conf, mocker, caplog, testdatadir) ->
mocker.patch('freqtrade.data.history.history_utils.load_pair_history',
MagicMock(return_value=pd.DataFrame()))
mocker.patch('freqtrade.data.history.get_timerange', get_timerange)
mocker.patch('freqtrade.exchange.Exchange.refresh_latest_ohlcv', MagicMock())
patch_exchange(mocker)
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', MagicMock())
mocker.patch('freqtrade.optimize.backtesting.generate_text_table', MagicMock(return_value=1))
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest')
mocker.patch('freqtrade.pairlist.pairlistmanager.PairListManager.whitelist',
PropertyMock(return_value=['UNITTEST/BTC']))
default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
default_conf['ticker_interval'] = "1m"
default_conf['datadir'] = testdatadir
default_conf['export'] = None
@@ -376,6 +377,29 @@ def test_backtesting_start_no_data(default_conf, mocker, caplog, testdatadir) ->
backtesting.start()
def test_backtesting_no_pair_left(default_conf, mocker, caplog, testdatadir) -> None:
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
mocker.patch('freqtrade.data.history.history_utils.load_pair_history',
MagicMock(return_value=pd.DataFrame()))
mocker.patch('freqtrade.data.history.get_timerange', get_timerange)
patch_exchange(mocker)
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest')
mocker.patch('freqtrade.pairlist.pairlistmanager.PairListManager.whitelist',
PropertyMock(return_value=[]))
default_conf['ticker_interval'] = "1m"
default_conf['datadir'] = testdatadir
default_conf['export'] = None
default_conf['timerange'] = '20180101-20180102'
with pytest.raises(OperationalException, match='No pair in whitelist.'):
Backtesting(default_conf)
default_conf['pairlists'] = [{"method": "VolumePairList", "number_assets": 5}]
with pytest.raises(OperationalException, match='VolumePairList not allowed for backtesting.'):
Backtesting(default_conf)
def test_backtest(default_conf, fee, mocker, testdatadir) -> None:
default_conf['ask_strategy']['use_sell_signal'] = False
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
@@ -385,10 +409,10 @@ def test_backtest(default_conf, fee, mocker, testdatadir) -> None:
timerange = TimeRange('date', None, 1517227800, 0)
data = history.load_data(datadir=testdatadir, timeframe='5m', pairs=['UNITTEST/BTC'],
timerange=timerange)
data_processed = backtesting.strategy.tickerdata_to_dataframe(data)
min_date, max_date = get_timerange(data_processed)
processed = backtesting.strategy.ohlcvdata_to_dataframe(data)
min_date, max_date = get_timerange(processed)
results = backtesting.backtest(
processed=data_processed,
processed=processed,
stake_amount=default_conf['stake_amount'],
start_date=min_date,
end_date=max_date,
@@ -416,7 +440,7 @@ def test_backtest(default_conf, fee, mocker, testdatadir) -> None:
'sell_reason': [SellType.ROI, SellType.ROI]
})
pd.testing.assert_frame_equal(results, expected)
data_pair = data_processed[pair]
data_pair = processed[pair]
for _, t in results.iterrows():
ln = data_pair.loc[data_pair["date"] == t["open_time"]]
# Check open trade rate alignes to open rate
@@ -439,7 +463,7 @@ def test_backtest_1min_ticker_interval(default_conf, fee, mocker, testdatadir) -
timerange = TimeRange.parse_timerange('1510688220-1510700340')
data = history.load_data(datadir=testdatadir, timeframe='1m', pairs=['UNITTEST/BTC'],
timerange=timerange)
processed = backtesting.strategy.tickerdata_to_dataframe(data)
processed = backtesting.strategy.ohlcvdata_to_dataframe(data)
min_date, max_date = get_timerange(processed)
results = backtesting.backtest(
processed=processed,
@@ -458,7 +482,7 @@ def test_processed(default_conf, mocker, testdatadir) -> None:
backtesting = Backtesting(default_conf)
dict_of_tickerrows = load_data_test('raise', testdatadir)
dataframes = backtesting.strategy.tickerdata_to_dataframe(dict_of_tickerrows)
dataframes = backtesting.strategy.ohlcvdata_to_dataframe(dict_of_tickerrows)
dataframe = dataframes['UNITTEST/BTC']
cols = dataframe.columns
# assert the dataframe got some of the indicator columns
@@ -508,7 +532,6 @@ def test_backtest_only_sell(mocker, default_conf, testdatadir):
def test_backtest_alternate_buy_sell(default_conf, fee, mocker, testdatadir):
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
mocker.patch('freqtrade.optimize.backtesting.file_dump_json', MagicMock())
backtest_conf = _make_backtest_conf(mocker, conf=default_conf,
pair='UNITTEST/BTC', datadir=testdatadir)
default_conf['ticker_interval'] = '1m'
@@ -516,7 +539,6 @@ def test_backtest_alternate_buy_sell(default_conf, fee, mocker, testdatadir):
backtesting.strategy.advise_buy = _trend_alternate # Override
backtesting.strategy.advise_sell = _trend_alternate # Override
results = backtesting.backtest(**backtest_conf)
backtesting._store_backtest_result("test_.json", results)
# 200 candles in backtest data
# won't buy on first (shifted by 1)
# 100 buys signals
@@ -533,7 +555,7 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir)
"""
Buy every xth candle - sell every other xth -2 (hold on to pairs a bit)
"""
if metadata['pair'] in('ETH/BTC', 'LTC/BTC'):
if metadata['pair'] in ('ETH/BTC', 'LTC/BTC'):
multi = 20
else:
multi = 18
@@ -557,10 +579,10 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir)
backtesting.strategy.advise_buy = _trend_alternate_hold # Override
backtesting.strategy.advise_sell = _trend_alternate_hold # Override
data_processed = backtesting.strategy.tickerdata_to_dataframe(data)
min_date, max_date = get_timerange(data_processed)
processed = backtesting.strategy.ohlcvdata_to_dataframe(data)
min_date, max_date = get_timerange(processed)
backtest_conf = {
'processed': data_processed,
'processed': processed,
'stake_amount': default_conf['stake_amount'],
'start_date': min_date,
'end_date': max_date,
@@ -576,7 +598,7 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir)
assert len(evaluate_result_multi(results, '5m', 3)) == 0
backtest_conf = {
'processed': data_processed,
'processed': processed,
'stake_amount': default_conf['stake_amount'],
'start_date': min_date,
'end_date': max_date,
@@ -587,85 +609,13 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir)
assert len(evaluate_result_multi(results, '5m', 1)) == 0
def test_backtest_record(default_conf, fee, mocker):
names = []
records = []
patch_exchange(mocker)
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
mocker.patch(
'freqtrade.optimize.backtesting.file_dump_json',
new=lambda n, r: (names.append(n), records.append(r))
)
backtesting = Backtesting(default_conf)
results = pd.DataFrame({"pair": ["UNITTEST/BTC", "UNITTEST/BTC",
"UNITTEST/BTC", "UNITTEST/BTC"],
"profit_percent": [0.003312, 0.010801, 0.013803, 0.002780],
"profit_abs": [0.000003, 0.000011, 0.000014, 0.000003],
"open_time": [Arrow(2017, 11, 14, 19, 32, 00).datetime,
Arrow(2017, 11, 14, 21, 36, 00).datetime,
Arrow(2017, 11, 14, 22, 12, 00).datetime,
Arrow(2017, 11, 14, 22, 44, 00).datetime],
"close_time": [Arrow(2017, 11, 14, 21, 35, 00).datetime,
Arrow(2017, 11, 14, 22, 10, 00).datetime,
Arrow(2017, 11, 14, 22, 43, 00).datetime,
Arrow(2017, 11, 14, 22, 58, 00).datetime],
"open_rate": [0.002543, 0.003003, 0.003089, 0.003214],
"close_rate": [0.002546, 0.003014, 0.003103, 0.003217],
"open_index": [1, 119, 153, 185],
"close_index": [118, 151, 184, 199],
"trade_duration": [123, 34, 31, 14],
"open_at_end": [False, False, False, True],
"sell_reason": [SellType.ROI, SellType.STOP_LOSS,
SellType.ROI, SellType.FORCE_SELL]
})
backtesting._store_backtest_result("backtest-result.json", results)
assert len(results) == 4
# Assert file_dump_json was only called once
assert names == ['backtest-result.json']
records = records[0]
# Ensure records are of correct type
assert len(records) == 4
# reset test to test with strategy name
names = []
records = []
backtesting._store_backtest_result(Path("backtest-result.json"), results, "DefStrat")
assert len(results) == 4
# Assert file_dump_json was only called once
assert names == [Path('backtest-result-DefStrat.json')]
records = records[0]
# Ensure records are of correct type
assert len(records) == 4
# ('UNITTEST/BTC', 0.00331158, '1510684320', '1510691700', 0, 117)
# Below follows just a typecheck of the schema/type of trade-records
oix = None
for (pair, profit, date_buy, date_sell, buy_index, dur,
openr, closer, open_at_end, sell_reason) in records:
assert pair == 'UNITTEST/BTC'
assert isinstance(profit, float)
# FIX: buy/sell should be converted to ints
assert isinstance(date_buy, float)
assert isinstance(date_sell, float)
assert isinstance(openr, float)
assert isinstance(closer, float)
assert isinstance(open_at_end, bool)
assert isinstance(sell_reason, str)
isinstance(buy_index, pd._libs.tslib.Timestamp)
if oix:
assert buy_index > oix
oix = buy_index
assert dur > 0
def test_backtest_start_timerange(default_conf, mocker, caplog, testdatadir):
default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
patch_exchange(mocker)
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', MagicMock())
mocker.patch('freqtrade.optimize.backtesting.generate_text_table', MagicMock())
mocker.patch('freqtrade.optimize.backtesting.show_backtest_results', MagicMock())
mocker.patch('freqtrade.pairlist.pairlistmanager.PairListManager.whitelist',
PropertyMock(return_value=['UNITTEST/BTC']))
patched_configuration_load_config_file(mocker, default_conf)
args = [
@@ -699,16 +649,19 @@ def test_backtest_start_timerange(default_conf, mocker, caplog, testdatadir):
assert log_has(line, caplog)
@pytest.mark.filterwarnings("ignore:deprecated")
def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir):
default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
patch_exchange(mocker)
backtestmock = MagicMock()
mocker.patch('freqtrade.pairlist.pairlistmanager.PairListManager.whitelist',
PropertyMock(return_value=['UNITTEST/BTC']))
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock)
gen_table_mock = MagicMock()
mocker.patch('freqtrade.optimize.backtesting.generate_text_table', gen_table_mock)
mocker.patch('freqtrade.optimize.optimize_reports.generate_text_table', gen_table_mock)
gen_strattable_mock = MagicMock()
mocker.patch('freqtrade.optimize.backtesting.generate_text_table_strategy', gen_strattable_mock)
mocker.patch('freqtrade.optimize.optimize_reports.generate_text_table_strategy',
gen_strattable_mock)
patched_configuration_load_config_file(mocker, default_conf)
args = [

View File

@@ -1,5 +1,6 @@
# pragma pylint: disable=missing-docstring,W0212,C0103
import locale
import logging
from datetime import datetime
from pathlib import Path
from typing import Dict, List
@@ -56,14 +57,14 @@ def hyperopt_results():
# Functions for recurrent object patching
def create_trials(mocker, hyperopt, testdatadir) -> List[Dict]:
def create_results(mocker, hyperopt, testdatadir) -> List[Dict]:
"""
When creating trials, mock the hyperopt Trials so that *by default*
When creating results, mock the hyperopt so that *by default*
- we don't create any pickle'd files in the filesystem
- we might have a pickle'd file so make sure that we return
false when looking for it
"""
hyperopt.trials_file = testdatadir / 'optimize/ut_trials.pickle'
hyperopt.results_file = testdatadir / 'optimize/ut_results.pickle'
mocker.patch.object(Path, "is_file", MagicMock(return_value=False))
stat_mock = MagicMock()
@@ -477,28 +478,30 @@ def test_no_log_if_loss_does_not_improve(hyperopt, caplog) -> None:
assert caplog.record_tuples == []
def test_save_trials_saves_trials(mocker, hyperopt, testdatadir, caplog) -> None:
trials = create_trials(mocker, hyperopt, testdatadir)
def test_save_results_saves_epochs(mocker, hyperopt, testdatadir, caplog) -> None:
epochs = create_results(mocker, hyperopt, testdatadir)
mock_dump = mocker.patch('freqtrade.optimize.hyperopt.dump', return_value=None)
trials_file = testdatadir / 'optimize' / 'ut_trials.pickle'
results_file = testdatadir / 'optimize' / 'ut_results.pickle'
hyperopt.trials = trials
hyperopt.save_trials(final=True)
assert log_has(f"1 epoch saved to '{trials_file}'.", caplog)
caplog.set_level(logging.DEBUG)
hyperopt.epochs = epochs
hyperopt._save_results()
assert log_has(f"1 epoch saved to '{results_file}'.", caplog)
mock_dump.assert_called_once()
hyperopt.trials = trials + trials
hyperopt.save_trials(final=True)
assert log_has(f"2 epochs saved to '{trials_file}'.", caplog)
hyperopt.epochs = epochs + epochs
hyperopt._save_results()
assert log_has(f"2 epochs saved to '{results_file}'.", caplog)
def test_read_trials_returns_trials_file(mocker, hyperopt, testdatadir, caplog) -> None:
trials = create_trials(mocker, hyperopt, testdatadir)
mock_load = mocker.patch('freqtrade.optimize.hyperopt.load', return_value=trials)
trials_file = testdatadir / 'optimize' / 'ut_trials.pickle'
hyperopt_trial = hyperopt._read_trials(trials_file)
assert log_has(f"Reading Trials from '{trials_file}'", caplog)
assert hyperopt_trial == trials
def test_read_results_returns_epochs(mocker, hyperopt, testdatadir, caplog) -> None:
epochs = create_results(mocker, hyperopt, testdatadir)
mock_load = mocker.patch('freqtrade.optimize.hyperopt.load', return_value=epochs)
results_file = testdatadir / 'optimize' / 'ut_results.pickle'
hyperopt_epochs = hyperopt._read_results(results_file)
assert log_has(f"Reading epochs from '{results_file}'", caplog)
assert hyperopt_epochs == epochs
mock_load.assert_called_once()
@@ -540,7 +543,7 @@ def test_start_calls_optimizer(mocker, default_conf, caplog, capsys) -> None:
}])
)
patch_exchange(mocker)
# Co-test loading ticker-interval from strategy
# Co-test loading timeframe from strategy
del default_conf['ticker_interval']
default_conf.update({'config': 'config.json.example',
'hyperopt': 'DefaultHyperOpt',
@@ -550,7 +553,7 @@ def test_start_calls_optimizer(mocker, default_conf, caplog, capsys) -> None:
'hyperopt_jobs': 1, })
hyperopt = Hyperopt(default_conf)
hyperopt.backtesting.strategy.tickerdata_to_dataframe = MagicMock()
hyperopt.backtesting.strategy.ohlcvdata_to_dataframe = MagicMock()
hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
hyperopt.start()
@@ -560,7 +563,7 @@ def test_start_calls_optimizer(mocker, default_conf, caplog, capsys) -> None:
out, err = capsys.readouterr()
assert 'Best result:\n\n* 1/1: foo result Objective: 1.00000\n' in out
assert dumper.called
# Should be called twice, once for tickerdata, once to save evaluations
# Should be called twice, once for historical candle data, once to save evaluations
assert dumper.call_count == 2
assert hasattr(hyperopt.backtesting.strategy, "advise_sell")
assert hasattr(hyperopt.backtesting.strategy, "advise_buy")
@@ -646,8 +649,8 @@ def test_has_space(hyperopt, spaces, expected_results):
def test_populate_indicators(hyperopt, testdatadir) -> None:
tickerlist = load_data(testdatadir, '1m', ['UNITTEST/BTC'], fill_up_missing=True)
dataframes = hyperopt.backtesting.strategy.tickerdata_to_dataframe(tickerlist)
data = load_data(testdatadir, '1m', ['UNITTEST/BTC'], fill_up_missing=True)
dataframes = hyperopt.backtesting.strategy.ohlcvdata_to_dataframe(data)
dataframe = hyperopt.custom_hyperopt.populate_indicators(dataframes['UNITTEST/BTC'],
{'pair': 'UNITTEST/BTC'})
@@ -658,8 +661,8 @@ def test_populate_indicators(hyperopt, testdatadir) -> None:
def test_buy_strategy_generator(hyperopt, testdatadir) -> None:
tickerlist = load_data(testdatadir, '1m', ['UNITTEST/BTC'], fill_up_missing=True)
dataframes = hyperopt.backtesting.strategy.tickerdata_to_dataframe(tickerlist)
data = load_data(testdatadir, '1m', ['UNITTEST/BTC'], fill_up_missing=True)
dataframes = hyperopt.backtesting.strategy.ohlcvdata_to_dataframe(data)
dataframe = hyperopt.custom_hyperopt.populate_indicators(dataframes['UNITTEST/BTC'],
{'pair': 'UNITTEST/BTC'})
@@ -799,7 +802,7 @@ def test_clean_hyperopt(mocker, default_conf, caplog):
h = Hyperopt(default_conf)
assert unlinkmock.call_count == 2
assert log_has(f"Removing `{h.tickerdata_pickle}`.", caplog)
assert log_has(f"Removing `{h.data_pickle_file}`.", caplog)
def test_continue_hyperopt(mocker, default_conf, caplog):
@@ -817,7 +820,7 @@ def test_continue_hyperopt(mocker, default_conf, caplog):
Hyperopt(default_conf)
assert unlinkmock.call_count == 0
assert log_has(f"Continuing on previous hyperopt results.", caplog)
assert log_has("Continuing on previous hyperopt results.", caplog)
def test_print_json_spaces_all(mocker, default_conf, caplog, capsys) -> None:
@@ -861,7 +864,7 @@ def test_print_json_spaces_all(mocker, default_conf, caplog, capsys) -> None:
})
hyperopt = Hyperopt(default_conf)
hyperopt.backtesting.strategy.tickerdata_to_dataframe = MagicMock()
hyperopt.backtesting.strategy.ohlcvdata_to_dataframe = MagicMock()
hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
hyperopt.start()
@@ -875,7 +878,7 @@ def test_print_json_spaces_all(mocker, default_conf, caplog, capsys) -> None:
)
assert result_str in out # noqa: E501
assert dumper.called
# Should be called twice, once for tickerdata, once to save evaluations
# Should be called twice, once for historical candle data, once to save evaluations
assert dumper.call_count == 2
@@ -919,7 +922,7 @@ def test_print_json_spaces_default(mocker, default_conf, caplog, capsys) -> None
})
hyperopt = Hyperopt(default_conf)
hyperopt.backtesting.strategy.tickerdata_to_dataframe = MagicMock()
hyperopt.backtesting.strategy.ohlcvdata_to_dataframe = MagicMock()
hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
hyperopt.start()
@@ -929,7 +932,7 @@ def test_print_json_spaces_default(mocker, default_conf, caplog, capsys) -> None
out, err = capsys.readouterr()
assert '{"params":{"mfi-value":null,"sell-mfi-value":null},"minimal_roi":{},"stoploss":null}' in out # noqa: E501
assert dumper.called
# Should be called twice, once for tickerdata, once to save evaluations
# Should be called twice, once for historical candle data, once to save evaluations
assert dumper.call_count == 2
@@ -969,7 +972,7 @@ def test_print_json_spaces_roi_stoploss(mocker, default_conf, caplog, capsys) ->
})
hyperopt = Hyperopt(default_conf)
hyperopt.backtesting.strategy.tickerdata_to_dataframe = MagicMock()
hyperopt.backtesting.strategy.ohlcvdata_to_dataframe = MagicMock()
hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
hyperopt.start()
@@ -979,7 +982,7 @@ def test_print_json_spaces_roi_stoploss(mocker, default_conf, caplog, capsys) ->
out, err = capsys.readouterr()
assert '{"minimal_roi":{},"stoploss":null}' in out
assert dumper.called
# Should be called twice, once for tickerdata, once to save evaluations
# Should be called twice, once for historical candle data, once to save evaluations
assert dumper.call_count == 2
@@ -1016,7 +1019,7 @@ def test_simplified_interface_roi_stoploss(mocker, default_conf, caplog, capsys)
'hyperopt_jobs': 1, })
hyperopt = Hyperopt(default_conf)
hyperopt.backtesting.strategy.tickerdata_to_dataframe = MagicMock()
hyperopt.backtesting.strategy.ohlcvdata_to_dataframe = MagicMock()
hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
del hyperopt.custom_hyperopt.__class__.buy_strategy_generator
@@ -1031,7 +1034,7 @@ def test_simplified_interface_roi_stoploss(mocker, default_conf, caplog, capsys)
out, err = capsys.readouterr()
assert 'Best result:\n\n* 1/1: foo result Objective: 1.00000\n' in out
assert dumper.called
# Should be called twice, once for tickerdata, once to save evaluations
# Should be called twice, once for historical candle data, once to save evaluations
assert dumper.call_count == 2
assert hasattr(hyperopt.backtesting.strategy, "advise_sell")
assert hasattr(hyperopt.backtesting.strategy, "advise_buy")
@@ -1059,7 +1062,7 @@ def test_simplified_interface_all_failed(mocker, default_conf, caplog, capsys) -
'hyperopt_jobs': 1, })
hyperopt = Hyperopt(default_conf)
hyperopt.backtesting.strategy.tickerdata_to_dataframe = MagicMock()
hyperopt.backtesting.strategy.ohlcvdata_to_dataframe = MagicMock()
hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
del hyperopt.custom_hyperopt.__class__.buy_strategy_generator
@@ -1104,7 +1107,7 @@ def test_simplified_interface_buy(mocker, default_conf, caplog, capsys) -> None:
'hyperopt_jobs': 1, })
hyperopt = Hyperopt(default_conf)
hyperopt.backtesting.strategy.tickerdata_to_dataframe = MagicMock()
hyperopt.backtesting.strategy.ohlcvdata_to_dataframe = MagicMock()
hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
# TODO: sell_strategy_generator() is actually not called because
@@ -1119,7 +1122,7 @@ def test_simplified_interface_buy(mocker, default_conf, caplog, capsys) -> None:
out, err = capsys.readouterr()
assert 'Best result:\n\n* 1/1: foo result Objective: 1.00000\n' in out
assert dumper.called
# Should be called twice, once for tickerdata, once to save evaluations
# Should be called twice, once for historical candle data, once to save evaluations
assert dumper.call_count == 2
assert hasattr(hyperopt.backtesting.strategy, "advise_sell")
assert hasattr(hyperopt.backtesting.strategy, "advise_buy")
@@ -1161,7 +1164,7 @@ def test_simplified_interface_sell(mocker, default_conf, caplog, capsys) -> None
'hyperopt_jobs': 1, })
hyperopt = Hyperopt(default_conf)
hyperopt.backtesting.strategy.tickerdata_to_dataframe = MagicMock()
hyperopt.backtesting.strategy.ohlcvdata_to_dataframe = MagicMock()
hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
# TODO: buy_strategy_generator() is actually not called because
@@ -1176,7 +1179,7 @@ def test_simplified_interface_sell(mocker, default_conf, caplog, capsys) -> None
out, err = capsys.readouterr()
assert 'Best result:\n\n* 1/1: foo result Objective: 1.00000\n' in out
assert dumper.called
# Should be called twice, once for tickerdata, once to save evaluations
# Should be called twice, once for historical candle data, once to save evaluations
assert dumper.call_count == 2
assert hasattr(hyperopt.backtesting.strategy, "advise_sell")
assert hasattr(hyperopt.backtesting.strategy, "advise_buy")
@@ -1210,7 +1213,7 @@ def test_simplified_interface_failed(mocker, default_conf, caplog, capsys, metho
'hyperopt_jobs': 1, })
hyperopt = Hyperopt(default_conf)
hyperopt.backtesting.strategy.tickerdata_to_dataframe = MagicMock()
hyperopt.backtesting.strategy.ohlcvdata_to_dataframe = MagicMock()
hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
delattr(hyperopt.custom_hyperopt.__class__, method)

View File

@@ -1,10 +1,14 @@
from pathlib import Path
import pandas as pd
from arrow import Arrow
from freqtrade.edge import PairInfo
from freqtrade.optimize.optimize_reports import (
generate_edge_table, generate_text_table, generate_text_table_sell_reason,
generate_text_table_strategy)
generate_text_table_strategy, store_backtest_result)
from freqtrade.strategy.interface import SellType
from tests.conftest import patch_exchange
def test_generate_text_table(default_conf, mocker):
@@ -61,10 +65,8 @@ def test_generate_text_table_sell_reason(default_conf, mocker):
'| stop_loss | 1 | 0 | 0 | 1 |'
' -10 | -10 | -0.2 | -5 |'
)
assert generate_text_table_sell_reason(
data={'ETH/BTC': {}},
stake_currency='BTC', max_open_trades=2,
results=results) == result_str
assert generate_text_table_sell_reason(stake_currency='BTC', max_open_trades=2,
results=results) == result_str
def test_generate_text_table_strategy(default_conf, mocker):
@@ -115,3 +117,77 @@ def test_generate_edge_table(edge_conf, mocker):
assert generate_edge_table(results).count('| ETH/BTC |') == 1
assert generate_edge_table(results).count(
'| Risk Reward Ratio | Required Risk Reward | Expectancy |') == 1
def test_backtest_record(default_conf, fee, mocker):
names = []
records = []
patch_exchange(mocker)
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
mocker.patch(
'freqtrade.optimize.optimize_reports.file_dump_json',
new=lambda n, r: (names.append(n), records.append(r))
)
results = {'DefStrat': pd.DataFrame({"pair": ["UNITTEST/BTC", "UNITTEST/BTC",
"UNITTEST/BTC", "UNITTEST/BTC"],
"profit_percent": [0.003312, 0.010801, 0.013803, 0.002780],
"profit_abs": [0.000003, 0.000011, 0.000014, 0.000003],
"open_time": [Arrow(2017, 11, 14, 19, 32, 00).datetime,
Arrow(2017, 11, 14, 21, 36, 00).datetime,
Arrow(2017, 11, 14, 22, 12, 00).datetime,
Arrow(2017, 11, 14, 22, 44, 00).datetime],
"close_time": [Arrow(2017, 11, 14, 21, 35, 00).datetime,
Arrow(2017, 11, 14, 22, 10, 00).datetime,
Arrow(2017, 11, 14, 22, 43, 00).datetime,
Arrow(2017, 11, 14, 22, 58, 00).datetime],
"open_rate": [0.002543, 0.003003, 0.003089, 0.003214],
"close_rate": [0.002546, 0.003014, 0.003103, 0.003217],
"open_index": [1, 119, 153, 185],
"close_index": [118, 151, 184, 199],
"trade_duration": [123, 34, 31, 14],
"open_at_end": [False, False, False, True],
"sell_reason": [SellType.ROI, SellType.STOP_LOSS,
SellType.ROI, SellType.FORCE_SELL]
})}
store_backtest_result(Path("backtest-result.json"), results)
# Assert file_dump_json was only called once
assert names == [Path('backtest-result.json')]
records = records[0]
# Ensure records are of correct type
assert len(records) == 4
# reset test to test with strategy name
names = []
records = []
results['Strat'] = results['DefStrat']
results['Strat2'] = results['DefStrat']
store_backtest_result(Path("backtest-result.json"), results)
assert names == [
Path('backtest-result-DefStrat.json'),
Path('backtest-result-Strat.json'),
Path('backtest-result-Strat2.json'),
]
records = records[0]
# Ensure records are of correct type
assert len(records) == 4
# ('UNITTEST/BTC', 0.00331158, '1510684320', '1510691700', 0, 117)
# Below follows just a typecheck of the schema/type of trade-records
oix = None
for (pair, profit, date_buy, date_sell, buy_index, dur,
openr, closer, open_at_end, sell_reason) in records:
assert pair == 'UNITTEST/BTC'
assert isinstance(profit, float)
# FIX: buy/sell should be converted to ints
assert isinstance(date_buy, float)
assert isinstance(date_sell, float)
assert isinstance(openr, float)
assert isinstance(closer, float)
assert isinstance(open_at_end, bool)
assert isinstance(sell_reason, str)
isinstance(buy_index, pd._libs.tslib.Timestamp)
if oix:
assert buy_index > oix
oix = buy_index
assert dur > 0

View File

@@ -4,13 +4,11 @@ from unittest.mock import MagicMock, PropertyMock
import pytest
from freqtrade.exceptions import OperationalException
from freqtrade.constants import AVAILABLE_PAIRLISTS
from freqtrade.resolvers import PairListResolver
from freqtrade.exceptions import OperationalException
from freqtrade.pairlist.pairlistmanager import PairListManager
from tests.conftest import get_patched_freqtradebot, log_has_re
# whitelist, blacklist
from freqtrade.resolvers import PairListResolver
from tests.conftest import get_patched_freqtradebot, log_has, log_has_re
@pytest.fixture(scope="function")
@@ -46,45 +44,67 @@ def static_pl_conf(whitelist_conf):
return whitelist_conf
def test_log_on_refresh(mocker, static_pl_conf, markets, tickers):
mocker.patch.multiple('freqtrade.exchange.Exchange',
markets=PropertyMock(return_value=markets),
exchange_has=MagicMock(return_value=True),
get_tickers=tickers
)
freqtrade = get_patched_freqtradebot(mocker, static_pl_conf)
logmock = MagicMock()
# Assign starting whitelist
pl = freqtrade.pairlists._pairlist_handlers[0]
pl.log_on_refresh(logmock, 'Hello world')
assert logmock.call_count == 1
pl.log_on_refresh(logmock, 'Hello world')
assert logmock.call_count == 1
assert pl._log_cache.currsize == 1
assert ('Hello world',) in pl._log_cache._Cache__data
pl.log_on_refresh(logmock, 'Hello world2')
assert logmock.call_count == 2
assert pl._log_cache.currsize == 2
def test_load_pairlist_noexist(mocker, markets, default_conf):
bot = get_patched_freqtradebot(mocker, default_conf)
freqtrade = get_patched_freqtradebot(mocker, default_conf)
mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets))
plm = PairListManager(bot.exchange, default_conf)
plm = PairListManager(freqtrade.exchange, default_conf)
with pytest.raises(OperationalException,
match=r"Impossible to load Pairlist 'NonexistingPairList'. "
r"This class does not exist or contains Python code errors."):
PairListResolver.load_pairlist('NonexistingPairList', bot.exchange, plm,
PairListResolver.load_pairlist('NonexistingPairList', freqtrade.exchange, plm,
default_conf, {}, 1)
def test_refresh_market_pair_not_in_whitelist(mocker, markets, static_pl_conf):
freqtradebot = get_patched_freqtradebot(mocker, static_pl_conf)
freqtrade = get_patched_freqtradebot(mocker, static_pl_conf)
mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets))
freqtradebot.pairlists.refresh_pairlist()
freqtrade.pairlists.refresh_pairlist()
# List ordered by BaseVolume
whitelist = ['ETH/BTC', 'TKN/BTC']
# Ensure all except those in whitelist are removed
assert set(whitelist) == set(freqtradebot.pairlists.whitelist)
assert set(whitelist) == set(freqtrade.pairlists.whitelist)
# Ensure config dict hasn't been changed
assert (static_pl_conf['exchange']['pair_whitelist'] ==
freqtradebot.config['exchange']['pair_whitelist'])
freqtrade.config['exchange']['pair_whitelist'])
def test_refresh_static_pairlist(mocker, markets, static_pl_conf):
freqtradebot = get_patched_freqtradebot(mocker, static_pl_conf)
freqtrade = get_patched_freqtradebot(mocker, static_pl_conf)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
exchange_has=MagicMock(return_value=True),
markets=PropertyMock(return_value=markets),
)
freqtradebot.pairlists.refresh_pairlist()
freqtrade.pairlists.refresh_pairlist()
# List ordered by BaseVolume
whitelist = ['ETH/BTC', 'TKN/BTC']
# Ensure all except those in whitelist are removed
assert set(whitelist) == set(freqtradebot.pairlists.whitelist)
assert static_pl_conf['exchange']['pair_blacklist'] == freqtradebot.pairlists.blacklist
assert set(whitelist) == set(freqtrade.pairlists.whitelist)
assert static_pl_conf['exchange']['pair_blacklist'] == freqtrade.pairlists.blacklist
def test_refresh_pairlist_dynamic(mocker, shitcoinmarkets, tickers, whitelist_conf):
@@ -94,7 +114,7 @@ def test_refresh_pairlist_dynamic(mocker, shitcoinmarkets, tickers, whitelist_co
get_tickers=tickers,
exchange_has=MagicMock(return_value=True),
)
bot = get_patched_freqtradebot(mocker, whitelist_conf)
freqtrade = get_patched_freqtradebot(mocker, whitelist_conf)
# Remock markets with shitcoinmarkets since get_patched_freqtradebot uses the markets fixture
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
@@ -102,9 +122,9 @@ def test_refresh_pairlist_dynamic(mocker, shitcoinmarkets, tickers, whitelist_co
)
# argument: use the whitelist dynamically by exchange-volume
whitelist = ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'XRP/BTC', 'HOT/BTC']
bot.pairlists.refresh_pairlist()
freqtrade.pairlists.refresh_pairlist()
assert whitelist == bot.pairlists.whitelist
assert whitelist == freqtrade.pairlists.whitelist
whitelist_conf['pairlists'] = [{'method': 'VolumePairList',
'config': {}
@@ -114,7 +134,7 @@ def test_refresh_pairlist_dynamic(mocker, shitcoinmarkets, tickers, whitelist_co
with pytest.raises(OperationalException,
match=r'`number_assets` not specified. Please check your configuration '
r'for "pairlist.config.number_assets"'):
PairListManager(bot.exchange, whitelist_conf)
PairListManager(freqtrade.exchange, whitelist_conf)
def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf):
@@ -122,29 +142,41 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf):
'freqtrade.exchange.Exchange',
exchange_has=MagicMock(return_value=True),
)
freqtradebot = get_patched_freqtradebot(mocker, whitelist_conf)
freqtrade = get_patched_freqtradebot(mocker, whitelist_conf)
mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets_empty))
# argument: use the whitelist dynamically by exchange-volume
whitelist = []
whitelist_conf['exchange']['pair_whitelist'] = []
freqtradebot.pairlists.refresh_pairlist()
freqtrade.pairlists.refresh_pairlist()
pairslist = whitelist_conf['exchange']['pair_whitelist']
assert set(whitelist) == set(pairslist)
@pytest.mark.parametrize("pairlists,base_currency,whitelist_result", [
# VolumePairList only
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}],
"BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'XRP/BTC', 'HOT/BTC']),
"BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'XRP/BTC', 'HOT/BTC']),
# Different sorting depending on quote or bid volume
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "bidVolume"}],
"BTC", ['HOT/BTC', 'FUEL/BTC', 'XRP/BTC', 'LTC/BTC', 'TKN/BTC']),
"BTC", ['HOT/BTC', 'FUEL/BTC', 'XRP/BTC', 'LTC/BTC', 'TKN/BTC']),
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}],
"USDT", ['ETH/USDT', 'NANO/USDT']),
# No pair for ETH ...
"USDT", ['ETH/USDT', 'NANO/USDT', 'ADAHALF/USDT']),
# No pair for ETH, VolumePairList
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}],
"ETH", []),
# No pair for ETH, StaticPairList
([{"method": "StaticPairList"}],
"ETH", []),
# No pair for ETH, all handlers
([{"method": "StaticPairList"},
{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"},
{"method": "PrecisionFilter"},
{"method": "PriceFilter", "low_price_ratio": 0.03},
{"method": "SpreadFilter", "max_spread_ratio": 0.005},
{"method": "ShuffleFilter"}],
"ETH", []),
# Precisionfilter and quote volume
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"},
{"method": "PrecisionFilter"}], "BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'XRP/BTC']),
@@ -154,32 +186,49 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf):
# PriceFilter and VolumePairList
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"},
{"method": "PriceFilter", "low_price_ratio": 0.03}],
"BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'XRP/BTC']),
"BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'XRP/BTC']),
# PriceFilter and VolumePairList
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"},
{"method": "PriceFilter", "low_price_ratio": 0.03}],
"USDT", ['ETH/USDT', 'NANO/USDT']),
# Hot is removed by precision_filter, Fuel by low_price_filter.
([{"method": "VolumePairList", "number_assets": 6, "sort_key": "quoteVolume"},
{"method": "PrecisionFilter"},
{"method": "PriceFilter", "low_price_ratio": 0.02}
], "BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'XRP/BTC']),
{"method": "PriceFilter", "low_price_ratio": 0.02}],
"BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'XRP/BTC']),
# HOT and XRP are removed because below 1250 quoteVolume
([{"method": "VolumePairList", "number_assets": 5,
"sort_key": "quoteVolume", "min_value": 1250}],
"BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC']),
# StaticPairlist Only
([{"method": "StaticPairList"},
], "BTC", ['ETH/BTC', 'TKN/BTC']),
"BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC']),
# StaticPairlist only
([{"method": "StaticPairList"}],
"BTC", ['ETH/BTC', 'TKN/BTC']),
# Static Pairlist before VolumePairList - sorting changes
([{"method": "StaticPairList"},
{"method": "VolumePairList", "number_assets": 5, "sort_key": "bidVolume"},
], "BTC", ['TKN/BTC', 'ETH/BTC']),
{"method": "VolumePairList", "number_assets": 5, "sort_key": "bidVolume"}],
"BTC", ['TKN/BTC', 'ETH/BTC']),
# SpreadFilter
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"},
{"method": "SpreadFilter", "max_spread": 0.005}
], "USDT", ['ETH/USDT']),
{"method": "SpreadFilter", "max_spread_ratio": 0.005}],
"USDT", ['ETH/USDT']),
# ShuffleFilter
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"},
{"method": "ShuffleFilter", "seed": 77}],
"USDT", ['ETH/USDT', 'ADAHALF/USDT', 'NANO/USDT']),
# ShuffleFilter, other seed
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"},
{"method": "ShuffleFilter", "seed": 42}],
"USDT", ['NANO/USDT', 'ETH/USDT', 'ADAHALF/USDT']),
# ShuffleFilter, no seed
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"},
{"method": "ShuffleFilter"}],
"USDT", 3),
])
def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, tickers,
pairlists, base_currency, whitelist_result,
caplog) -> None:
whitelist_conf['pairlists'] = pairlists
whitelist_conf['stake_currency'] = base_currency
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
freqtrade = get_patched_freqtradebot(mocker, whitelist_conf)
@@ -189,17 +238,32 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t
markets=PropertyMock(return_value=shitcoinmarkets),
)
freqtrade.config['stake_currency'] = base_currency
freqtrade.pairlists.refresh_pairlist()
whitelist = freqtrade.pairlists.whitelist
assert whitelist == whitelist_result
assert isinstance(whitelist, list)
# Verify length of pairlist matches (used for ShuffleFilter without seed)
if type(whitelist_result) is list:
assert whitelist == whitelist_result
else:
len(whitelist) == whitelist_result
for pairlist in pairlists:
if pairlist['method'] == 'PrecisionFilter':
if pairlist['method'] == 'PrecisionFilter' and whitelist_result:
assert log_has_re(r'^Removed .* from whitelist, because stop price .* '
r'would be <= stop limit.*', caplog)
if pairlist['method'] == 'PriceFilter':
assert log_has_re(r'^Removed .* from whitelist, because 1 unit is .*%$', caplog)
if pairlist['method'] == 'PriceFilter' and whitelist_result:
assert (log_has_re(r'^Removed .* from whitelist, because 1 unit is .*%$', caplog) or
log_has_re(r"^Removed .* from whitelist, because ticker\['last'\] is empty.*",
caplog))
if pairlist['method'] == 'VolumePairList':
logmsg = ("DEPRECATED: using any key other than quoteVolume for "
"VolumePairList is deprecated.")
if pairlist['sort_key'] != 'quoteVolume':
assert log_has(logmsg, caplog)
else:
assert not log_has(logmsg, caplog)
def test_gen_pair_whitelist_not_supported(mocker, default_conf, tickers) -> None:
@@ -255,7 +319,8 @@ def test__whitelist_for_active_markets(mocker, whitelist_conf, markets, pairlist
caplog.clear()
# Assign starting whitelist
new_whitelist = freqtrade.pairlists._pairlists[0]._whitelist_for_active_markets(whitelist)
pairlist_handler = freqtrade.pairlists._pairlist_handlers[0]
new_whitelist = pairlist_handler._whitelist_for_active_markets(whitelist)
assert set(new_whitelist) == set(['ETH/BTC', 'TKN/BTC'])
assert log_message in caplog.text
@@ -277,18 +342,18 @@ def test_volumepairlist_caching(mocker, markets, whitelist_conf, tickers):
exchange_has=MagicMock(return_value=True),
get_tickers=tickers
)
bot = get_patched_freqtradebot(mocker, whitelist_conf)
assert bot.pairlists._pairlists[0]._last_refresh == 0
freqtrade = get_patched_freqtradebot(mocker, whitelist_conf)
assert freqtrade.pairlists._pairlist_handlers[0]._last_refresh == 0
assert tickers.call_count == 0
bot.pairlists.refresh_pairlist()
freqtrade.pairlists.refresh_pairlist()
assert tickers.call_count == 1
assert bot.pairlists._pairlists[0]._last_refresh != 0
lrf = bot.pairlists._pairlists[0]._last_refresh
bot.pairlists.refresh_pairlist()
assert freqtrade.pairlists._pairlist_handlers[0]._last_refresh != 0
lrf = freqtrade.pairlists._pairlist_handlers[0]._last_refresh
freqtrade.pairlists.refresh_pairlist()
assert tickers.call_count == 1
# Time should not be updated.
assert bot.pairlists._pairlists[0]._last_refresh == lrf
assert freqtrade.pairlists._pairlist_handlers[0]._last_refresh == lrf
def test_pairlistmanager_no_pairlist(mocker, markets, whitelist_conf, caplog):
@@ -297,5 +362,5 @@ def test_pairlistmanager_no_pairlist(mocker, markets, whitelist_conf, caplog):
whitelist_conf['pairlists'] = []
with pytest.raises(OperationalException,
match=r"No Pairlist defined!"):
match=r"No Pairlist Handlers defined"):
get_patched_freqtradebot(mocker, whitelist_conf)

View File

@@ -13,7 +13,7 @@ from freqtrade.persistence import Trade
from freqtrade.rpc import RPC, RPCException
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
from freqtrade.state import State
from tests.conftest import get_patched_freqtradebot, patch_get_signal
from tests.conftest import get_patched_freqtradebot, patch_get_signal, create_mock_trades
# Functions for recurrent object patching
@@ -49,6 +49,23 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
'base_currency': 'BTC',
'open_date': ANY,
'open_date_hum': ANY,
'is_open': ANY,
'fee_open': ANY,
'fee_open_cost': ANY,
'fee_open_currency': ANY,
'fee_close': ANY,
'fee_close_cost': ANY,
'fee_close_currency': ANY,
'open_rate_requested': ANY,
'open_trade_price': ANY,
'close_rate_requested': ANY,
'sell_reason': ANY,
'sell_order_status': ANY,
'min_rate': ANY,
'max_rate': ANY,
'strategy': ANY,
'ticker_interval': ANY,
'open_order_id': ANY,
'close_date': None,
'close_date_hum': None,
'open_rate': 1.098e-05,
@@ -66,7 +83,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
} == results[0]
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate',
MagicMock(side_effect=DependencyException(f"Pair 'ETH/BTC' not available")))
MagicMock(side_effect=DependencyException("Pair 'ETH/BTC' not available")))
results = rpc._rpc_trade_status()
assert isnan(results[0]['current_profit'])
assert isnan(results[0]['current_rate'])
@@ -76,6 +93,23 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
'base_currency': 'BTC',
'open_date': ANY,
'open_date_hum': ANY,
'is_open': ANY,
'fee_open': ANY,
'fee_open_cost': ANY,
'fee_open_currency': ANY,
'fee_close': ANY,
'fee_close_cost': ANY,
'fee_close_currency': ANY,
'open_rate_requested': ANY,
'open_trade_price': ANY,
'close_rate_requested': ANY,
'sell_reason': ANY,
'sell_order_status': ANY,
'min_rate': ANY,
'max_rate': ANY,
'strategy': ANY,
'ticker_interval': ANY,
'open_order_id': ANY,
'close_date': None,
'close_date_hum': None,
'open_rate': 1.098e-05,
@@ -133,7 +167,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None:
assert '-0.41% (-0.06)' == result[0][3]
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate',
MagicMock(side_effect=DependencyException(f"Pair 'ETH/BTC' not available")))
MagicMock(side_effect=DependencyException("Pair 'ETH/BTC' not available")))
result, headers = rpc._rpc_status_table(default_conf['stake_currency'], 'USD')
assert 'instantly' == result[0][2]
assert 'ETH/BTC' in result[0][1]
@@ -171,22 +205,50 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee,
# Try valid data
update.message.text = '/daily 2'
days = rpc._rpc_daily_profit(7, stake_currency, fiat_display_currency)
assert len(days) == 7
for day in days:
assert len(days['data']) == 7
assert days['stake_currency'] == default_conf['stake_currency']
assert days['fiat_display_currency'] == default_conf['fiat_display_currency']
for day in days['data']:
# [datetime.date(2018, 1, 11), '0.00000000 BTC', '0.000 USD']
assert (day[1] == '0.00000000 BTC' or
day[1] == '0.00006217 BTC')
assert (day['abs_profit'] == '0.00000000' or
day['abs_profit'] == '0.00006217')
assert (day[2] == '0.000 USD' or
day[2] == '0.767 USD')
assert (day['fiat_value'] == '0.000' or
day['fiat_value'] == '0.767')
# ensure first day is current date
assert str(days[0][0]) == str(datetime.utcnow().date())
assert str(days['data'][0]['date']) == str(datetime.utcnow().date())
# Try invalid data
with pytest.raises(RPCException, match=r'.*must be an integer greater than 0*'):
rpc._rpc_daily_profit(0, stake_currency, fiat_display_currency)
def test_rpc_trade_history(mocker, default_conf, markets, fee):
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
markets=PropertyMock(return_value=markets)
)
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
create_mock_trades(fee)
rpc = RPC(freqtradebot)
rpc._fiat_converter = CryptoToFiatConverter()
trades = rpc._rpc_trade_history(2)
assert len(trades['trades']) == 2
assert trades['trades_count'] == 2
assert isinstance(trades['trades'][0], dict)
assert isinstance(trades['trades'][1], dict)
trades = rpc._rpc_trade_history(0)
assert len(trades['trades']) == 3
assert trades['trades_count'] == 3
# The first trade is for ETH ... sorting is descending
assert trades['trades'][-1]['pair'] == 'ETH/BTC'
assert trades['trades'][0]['pair'] == 'ETC/BTC'
assert trades['trades'][1]['pair'] == 'ETC/BTC'
def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
limit_buy_order, limit_sell_order, mocker) -> None:
mocker.patch.multiple(
@@ -257,7 +319,7 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
# Test non-available pair
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate',
MagicMock(side_effect=DependencyException(f"Pair 'ETH/BTC' not available")))
MagicMock(side_effect=DependencyException("Pair 'ETH/BTC' not available")))
stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
assert stats['trade_count'] == 2
assert stats['first_trade_date'] == 'just now'

View File

@@ -13,7 +13,7 @@ from freqtrade.__init__ import __version__
from freqtrade.persistence import Trade
from freqtrade.rpc.api_server import BASE_URI, ApiServer
from freqtrade.state import State
from tests.conftest import get_patched_freqtradebot, log_has, patch_get_signal
from tests.conftest import get_patched_freqtradebot, log_has, patch_get_signal, create_mock_trades
_TEST_USER = "FreqTrader"
_TEST_PASS = "SuperSecurePassword1!"
@@ -49,6 +49,7 @@ def client_get(client, url):
def assert_response(response, expected_code=200):
assert response.status_code == expected_code
assert response.content_type == "application/json"
assert ('Access-Control-Allow-Origin', '*') in response.headers._list
def test_api_not_found(botclient):
@@ -94,6 +95,33 @@ def test_api_unauthorized(botclient):
assert rc.json == {'error': 'Unauthorized'}
def test_api_token_login(botclient):
ftbot, client = botclient
rc = client_post(client, f"{BASE_URI}/token/login")
assert_response(rc)
assert 'access_token' in rc.json
assert 'refresh_token' in rc.json
# test Authentication is working with JWT tokens too
rc = client.get(f"{BASE_URI}/count",
content_type="application/json",
headers={'Authorization': f'Bearer {rc.json["access_token"]}'})
assert_response(rc)
def test_api_token_refresh(botclient):
ftbot, client = botclient
rc = client_post(client, f"{BASE_URI}/token/login")
assert_response(rc)
rc = client.post(f"{BASE_URI}/token/refresh",
content_type="application/json",
data=None,
headers={'Authorization': f'Bearer {rc.json["refresh_token"]}'})
assert_response(rc)
assert 'access_token' in rc.json
assert 'refresh_token' not in rc.json
def test_api_stop_workflow(botclient):
ftbot, client = botclient
assert ftbot.state == State.RUNNING
@@ -123,6 +151,12 @@ def test_api__init__(default_conf, mocker):
"""
Test __init__() method
"""
default_conf.update({"api_server": {"enabled": True,
"listen_ip_address": "127.0.0.1",
"listen_port": 8080,
"username": "TestUser",
"password": "testPass",
}})
mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock())
mocker.patch('freqtrade.rpc.api_server.ApiServer.run', MagicMock())
@@ -283,6 +317,7 @@ def test_api_show_config(botclient, mocker):
assert 'dry_run' in rc.json
assert rc.json['exchange'] == 'bittrex'
assert rc.json['ticker_interval'] == '5m'
assert rc.json['state'] == 'running'
assert not rc.json['trailing_stop']
@@ -298,8 +333,34 @@ def test_api_daily(botclient, mocker, ticker, fee, markets):
)
rc = client_get(client, f"{BASE_URI}/daily")
assert_response(rc)
assert len(rc.json) == 7
assert rc.json[0][0] == str(datetime.utcnow().date())
assert len(rc.json['data']) == 7
assert rc.json['stake_currency'] == 'BTC'
assert rc.json['fiat_display_currency'] == 'USD'
assert rc.json['data'][0]['date'] == str(datetime.utcnow().date())
def test_api_trades(botclient, mocker, ticker, fee, markets):
ftbot, client = botclient
patch_get_signal(ftbot, (True, False))
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
markets=PropertyMock(return_value=markets)
)
rc = client_get(client, f"{BASE_URI}/trades")
assert_response(rc)
assert len(rc.json) == 2
assert rc.json['trades_count'] == 0
create_mock_trades(fee)
rc = client_get(client, f"{BASE_URI}/trades")
assert_response(rc)
assert len(rc.json['trades']) == 3
assert rc.json['trades_count'] == 3
rc = client_get(client, f"{BASE_URI}/trades?limit=2")
assert_response(rc)
assert len(rc.json['trades']) == 2
assert rc.json['trades_count'] == 2
def test_api_edge_disabled(botclient, mocker, ticker, fee, markets):
@@ -444,7 +505,26 @@ def test_api_status(botclient, mocker, ticker, fee, markets):
'stake_amount': 0.001,
'stop_loss': 0.0,
'stop_loss_pct': None,
'trade_id': 1}]
'trade_id': 1,
'close_rate_requested': None,
'current_rate': 1.099e-05,
'fee_close': 0.0025,
'fee_close_cost': None,
'fee_close_currency': None,
'fee_open': 0.0025,
'fee_open_cost': None,
'fee_open_currency': None,
'open_date': ANY,
'is_open': True,
'max_rate': 0.0,
'min_rate': None,
'open_order_id': ANY,
'open_rate_requested': 1.098e-05,
'open_trade_price': 0.0010025,
'sell_reason': None,
'sell_order_status': None,
'strategy': 'DefaultStrategy',
'ticker_interval': 5}]
def test_api_version(botclient):
@@ -533,7 +613,26 @@ def test_api_forcebuy(botclient, mocker, fee):
'stake_amount': 1,
'stop_loss': None,
'stop_loss_pct': None,
'trade_id': None}
'trade_id': None,
'close_profit': None,
'close_rate_requested': None,
'fee_close': 0.0025,
'fee_close_cost': None,
'fee_close_currency': None,
'fee_open': 0.0025,
'fee_open_cost': None,
'fee_open_currency': None,
'is_open': False,
'max_rate': None,
'min_rate': None,
'open_order_id': '123456',
'open_rate_requested': None,
'open_trade_price': 0.2460546025,
'sell_reason': None,
'sell_order_status': None,
'strategy': None,
'ticker_interval': None
}
def test_api_forcesell(botclient, mocker, ticker, fee, markets):

View File

@@ -170,6 +170,7 @@ def test_status(default_conf, update, mocker, fee, ticker,) -> None:
'current_profit': -0.59,
'initial_stop_loss': 1.098e-05,
'stop_loss': 1.099e-05,
'sell_order_status': None,
'initial_stop_loss_pct': -0.05,
'stop_loss_pct': -0.01,
'open_order': '(limit buy rem=0.00000000)'
@@ -1316,18 +1317,20 @@ def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None:
'type': RPCMessageType.SELL_CANCEL_NOTIFICATION,
'exchange': 'Binance',
'pair': 'KEY/ETH',
'reason': 'Cancelled on exchange'
})
assert msg_mock.call_args[0][0] \
== ('*Binance:* Cancelling Open Sell Order for KEY/ETH')
== ('*Binance:* Cancelling Open Sell Order for KEY/ETH. Reason: Cancelled on exchange')
msg_mock.reset_mock()
telegram.send_msg({
'type': RPCMessageType.SELL_CANCEL_NOTIFICATION,
'exchange': 'Binance',
'pair': 'KEY/ETH',
'reason': 'timeout'
})
assert msg_mock.call_args[0][0] \
== ('*Binance:* Cancelling Open Sell Order for KEY/ETH')
== ('*Binance:* Cancelling Open Sell Order for KEY/ETH. Reason: timeout')
# Reset singleton function to avoid random breaks
telegram._fiat_converter.convert_amount = old_convamount

View File

@@ -68,7 +68,7 @@ class DefaultStrategy(IStrategy):
Performance Note: For the best performance be frugal on the number of indicators
you are using. Let uncomment only the indicator you are using in your strategies
or your hyperopt configuration, otherwise you will waste your memory and CPU usage.
:param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe()
:param dataframe: Dataframe with data from the exchange
:param metadata: Additional information, like the currently traded pair
:return: a Dataframe with all mandatory indicators for the strategies
"""

View File

@@ -4,82 +4,91 @@ import logging
from unittest.mock import MagicMock
import arrow
import pytest
from pandas import DataFrame
from freqtrade.configuration import TimeRange
from freqtrade.data.history import load_data
from freqtrade.exceptions import StrategyError
from freqtrade.persistence import Trade
from freqtrade.resolvers import StrategyResolver
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
from tests.conftest import get_patched_exchange, log_has, log_has_re
from .strats.default_strategy import DefaultStrategy
from tests.conftest import get_patched_exchange, log_has
# Avoid to reinit the same object again and again
_STRATEGY = DefaultStrategy(config={})
def test_returns_latest_buy_signal(mocker, default_conf, ticker_history):
mocker.patch.object(
_STRATEGY, '_analyze_ticker_internal',
return_value=DataFrame([{'buy': 1, 'sell': 0, 'date': arrow.utcnow()}])
)
assert _STRATEGY.get_signal('ETH/BTC', '5m', ticker_history) == (True, False)
def test_returns_latest_signal(mocker, default_conf, ohlcv_history):
ohlcv_history.loc[1, 'date'] = arrow.utcnow()
# Take a copy to correctly modify the call
mocked_history = ohlcv_history.copy()
mocked_history['sell'] = 0
mocked_history['buy'] = 0
mocked_history.loc[1, 'sell'] = 1
mocker.patch.object(
_STRATEGY, '_analyze_ticker_internal',
return_value=DataFrame([{'buy': 0, 'sell': 1, 'date': arrow.utcnow()}])
)
assert _STRATEGY.get_signal('ETH/BTC', '5m', ticker_history) == (False, True)
def test_returns_latest_sell_signal(mocker, default_conf, ticker_history):
mocker.patch.object(
_STRATEGY, '_analyze_ticker_internal',
return_value=DataFrame([{'sell': 1, 'buy': 0, 'date': arrow.utcnow()}])
return_value=mocked_history
)
assert _STRATEGY.get_signal('ETH/BTC', '5m', ticker_history) == (False, True)
assert _STRATEGY.get_signal('ETH/BTC', '5m', ohlcv_history) == (False, True)
mocked_history.loc[1, 'sell'] = 0
mocked_history.loc[1, 'buy'] = 1
mocker.patch.object(
_STRATEGY, '_analyze_ticker_internal',
return_value=DataFrame([{'sell': 0, 'buy': 1, 'date': arrow.utcnow()}])
return_value=mocked_history
)
assert _STRATEGY.get_signal('ETH/BTC', '5m', ticker_history) == (True, False)
assert _STRATEGY.get_signal('ETH/BTC', '5m', ohlcv_history) == (True, False)
mocked_history.loc[1, 'sell'] = 0
mocked_history.loc[1, 'buy'] = 0
mocker.patch.object(
_STRATEGY, '_analyze_ticker_internal',
return_value=mocked_history
)
assert _STRATEGY.get_signal('ETH/BTC', '5m', ohlcv_history) == (False, False)
def test_get_signal_empty(default_conf, mocker, caplog):
assert (False, False) == _STRATEGY.get_signal('foo', default_conf['ticker_interval'],
DataFrame())
assert log_has('Empty ticker history for pair foo', caplog)
assert log_has('Empty candle (OHLCV) data for pair foo', caplog)
caplog.clear()
assert (False, False) == _STRATEGY.get_signal('bar', default_conf['ticker_interval'],
[])
assert log_has('Empty ticker history for pair bar', caplog)
assert log_has('Empty candle (OHLCV) data for pair bar', caplog)
def test_get_signal_exception_valueerror(default_conf, mocker, caplog, ticker_history):
def test_get_signal_exception_valueerror(default_conf, mocker, caplog, ohlcv_history):
caplog.set_level(logging.INFO)
mocker.patch.object(
_STRATEGY, '_analyze_ticker_internal',
side_effect=ValueError('xyz')
)
assert (False, False) == _STRATEGY.get_signal('foo', default_conf['ticker_interval'],
ticker_history)
assert log_has('Unable to analyze ticker for pair foo: xyz', caplog)
ohlcv_history)
assert log_has_re(r'Strategy caused the following exception: xyz.*', caplog)
def test_get_signal_empty_dataframe(default_conf, mocker, caplog, ticker_history):
def test_get_signal_empty_dataframe(default_conf, mocker, caplog, ohlcv_history):
caplog.set_level(logging.INFO)
mocker.patch.object(
_STRATEGY, '_analyze_ticker_internal',
return_value=DataFrame([])
)
mocker.patch.object(_STRATEGY, 'assert_df')
assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['ticker_interval'],
ticker_history)
ohlcv_history)
assert log_has('Empty dataframe for pair xyz', caplog)
def test_get_signal_old_candle(default_conf, mocker, caplog, ticker_history):
def test_get_signal_old_candle(default_conf, mocker, caplog, ohlcv_history):
caplog.set_level(logging.INFO)
# default_conf defines a 5m interval. we check interval of previous candle
# this is necessary as the last candle is removed (partial candles) by default
@@ -90,23 +99,69 @@ def test_get_signal_old_candle(default_conf, mocker, caplog, ticker_history):
return_value=DataFrame(ticks)
)
assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['ticker_interval'],
ticker_history)
ohlcv_history)
assert log_has('Old candle for pair xyz. Last candle is 10 minutes old', caplog)
def test_get_signal_old_dataframe(default_conf, mocker, caplog, ticker_history):
caplog.set_level(logging.INFO)
def test_get_signal_old_dataframe(default_conf, mocker, caplog, ohlcv_history):
# default_conf defines a 5m interval. we check interval * 2 + 5m
# this is necessary as the last candle is removed (partial candles) by default
oldtime = arrow.utcnow().shift(minutes=-16)
ticks = DataFrame([{'buy': 1, 'date': oldtime}])
ohlcv_history.loc[1, 'date'] = arrow.utcnow().shift(minutes=-16)
# Take a copy to correctly modify the call
mocked_history = ohlcv_history.copy()
mocked_history['sell'] = 0
mocked_history['buy'] = 0
mocked_history.loc[1, 'buy'] = 1
caplog.set_level(logging.INFO)
mocker.patch.object(
_STRATEGY, '_analyze_ticker_internal',
return_value=DataFrame(ticks)
return_value=mocked_history
)
mocker.patch.object(_STRATEGY, 'assert_df')
assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['ticker_interval'],
ohlcv_history)
assert log_has('Outdated history for pair xyz. Last tick is 16 minutes old', caplog)
def test_assert_df_raise(default_conf, mocker, caplog, ohlcv_history):
# default_conf defines a 5m interval. we check interval * 2 + 5m
# this is necessary as the last candle is removed (partial candles) by default
ohlcv_history.loc[1, 'date'] = arrow.utcnow().shift(minutes=-16)
# Take a copy to correctly modify the call
mocked_history = ohlcv_history.copy()
mocked_history['sell'] = 0
mocked_history['buy'] = 0
mocked_history.loc[1, 'buy'] = 1
caplog.set_level(logging.INFO)
mocker.patch.object(
_STRATEGY, 'assert_df',
side_effect=StrategyError('Dataframe returned...')
)
assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['ticker_interval'],
ticker_history)
assert log_has('Outdated history for pair xyz. Last tick is 16 minutes old', caplog)
ohlcv_history)
assert log_has('Unable to analyze candle (OHLCV) data for pair xyz: Dataframe returned...',
caplog)
def test_assert_df(default_conf, mocker, ohlcv_history):
# Ensure it's running when passed correctly
_STRATEGY.assert_df(ohlcv_history, len(ohlcv_history),
ohlcv_history.loc[1, 'close'], ohlcv_history.loc[1, 'date'])
with pytest.raises(StrategyError, match=r"Dataframe returned from strategy.*length\."):
_STRATEGY.assert_df(ohlcv_history, len(ohlcv_history) + 1,
ohlcv_history.loc[1, 'close'], ohlcv_history.loc[1, 'date'])
with pytest.raises(StrategyError,
match=r"Dataframe returned from strategy.*last close price\."):
_STRATEGY.assert_df(ohlcv_history, len(ohlcv_history),
ohlcv_history.loc[1, 'close'] + 0.01, ohlcv_history.loc[1, 'date'])
with pytest.raises(StrategyError,
match=r"Dataframe returned from strategy.*last date\."):
_STRATEGY.assert_df(ohlcv_history, len(ohlcv_history),
ohlcv_history.loc[1, 'close'], ohlcv_history.loc[0, 'date'])
def test_get_signal_handles_exceptions(mocker, default_conf):
@@ -118,15 +173,28 @@ def test_get_signal_handles_exceptions(mocker, default_conf):
assert _STRATEGY.get_signal(exchange, 'ETH/BTC', '5m') == (False, False)
def test_tickerdata_to_dataframe(default_conf, testdatadir) -> None:
def test_ohlcvdata_to_dataframe(default_conf, testdatadir) -> None:
default_conf.update({'strategy': 'DefaultStrategy'})
strategy = StrategyResolver.load_strategy(default_conf)
timerange = TimeRange.parse_timerange('1510694220-1510700340')
tickerlist = load_data(testdatadir, '1m', ['UNITTEST/BTC'], timerange=timerange,
fill_up_missing=True)
data = strategy.tickerdata_to_dataframe(tickerlist)
assert len(data['UNITTEST/BTC']) == 102 # partial candle was removed
data = load_data(testdatadir, '1m', ['UNITTEST/BTC'], timerange=timerange,
fill_up_missing=True)
processed = strategy.ohlcvdata_to_dataframe(data)
assert len(processed['UNITTEST/BTC']) == 102 # partial candle was removed
def test_ohlcvdata_to_dataframe_copy(mocker, default_conf, testdatadir) -> None:
default_conf.update({'strategy': 'DefaultStrategy'})
strategy = StrategyResolver.load_strategy(default_conf)
aimock = mocker.patch('freqtrade.strategy.interface.IStrategy.advise_indicators')
timerange = TimeRange.parse_timerange('1510694220-1510700340')
data = load_data(testdatadir, '1m', ['UNITTEST/BTC'], timerange=timerange,
fill_up_missing=True)
strategy.ohlcvdata_to_dataframe(data)
assert aimock.call_count == 1
# Ensure that a copy of the dataframe is passed to advice_indicators
assert aimock.call_args_list[0][0][0] is not data
def test_min_roi_reached(default_conf, fee) -> None:
@@ -237,7 +305,7 @@ def test_min_roi_reached3(default_conf, fee) -> None:
assert strategy.min_roi_reached(trade, 0.31, arrow.utcnow().shift(minutes=-2).datetime)
def test_analyze_ticker_default(ticker_history, mocker, caplog) -> None:
def test_analyze_ticker_default(ohlcv_history, mocker, caplog) -> None:
caplog.set_level(logging.DEBUG)
ind_mock = MagicMock(side_effect=lambda x, meta: x)
buy_mock = MagicMock(side_effect=lambda x, meta: x)
@@ -250,7 +318,7 @@ def test_analyze_ticker_default(ticker_history, mocker, caplog) -> None:
)
strategy = DefaultStrategy({})
strategy.analyze_ticker(ticker_history, {'pair': 'ETH/BTC'})
strategy.analyze_ticker(ohlcv_history, {'pair': 'ETH/BTC'})
assert ind_mock.call_count == 1
assert buy_mock.call_count == 1
assert buy_mock.call_count == 1
@@ -259,7 +327,7 @@ def test_analyze_ticker_default(ticker_history, mocker, caplog) -> None:
assert not log_has('Skipping TA Analysis for already analyzed candle', caplog)
caplog.clear()
strategy.analyze_ticker(ticker_history, {'pair': 'ETH/BTC'})
strategy.analyze_ticker(ohlcv_history, {'pair': 'ETH/BTC'})
# No analysis happens as process_only_new_candles is true
assert ind_mock.call_count == 2
assert buy_mock.call_count == 2
@@ -268,7 +336,7 @@ def test_analyze_ticker_default(ticker_history, mocker, caplog) -> None:
assert not log_has('Skipping TA Analysis for already analyzed candle', caplog)
def test__analyze_ticker_internal_skip_analyze(ticker_history, mocker, caplog) -> None:
def test__analyze_ticker_internal_skip_analyze(ohlcv_history, mocker, caplog) -> None:
caplog.set_level(logging.DEBUG)
ind_mock = MagicMock(side_effect=lambda x, meta: x)
buy_mock = MagicMock(side_effect=lambda x, meta: x)
@@ -283,7 +351,7 @@ def test__analyze_ticker_internal_skip_analyze(ticker_history, mocker, caplog) -
strategy = DefaultStrategy({})
strategy.process_only_new_candles = True
ret = strategy._analyze_ticker_internal(ticker_history, {'pair': 'ETH/BTC'})
ret = strategy._analyze_ticker_internal(ohlcv_history, {'pair': 'ETH/BTC'})
assert 'high' in ret.columns
assert 'low' in ret.columns
assert 'close' in ret.columns
@@ -295,7 +363,7 @@ def test__analyze_ticker_internal_skip_analyze(ticker_history, mocker, caplog) -
assert not log_has('Skipping TA Analysis for already analyzed candle', caplog)
caplog.clear()
ret = strategy._analyze_ticker_internal(ticker_history, {'pair': 'ETH/BTC'})
ret = strategy._analyze_ticker_internal(ohlcv_history, {'pair': 'ETH/BTC'})
# No analysis happens as process_only_new_candles is true
assert ind_mock.call_count == 1
assert buy_mock.call_count == 1
@@ -337,3 +405,38 @@ def test_is_pair_locked(default_conf):
pair = 'ETH/BTC'
strategy.unlock_pair(pair)
assert not strategy.is_pair_locked(pair)
@pytest.mark.parametrize('error', [
ValueError, KeyError, Exception,
])
def test_strategy_safe_wrapper_error(caplog, error):
def failing_method():
raise error('This is an error.')
def working_method(argumentpassedin):
return argumentpassedin
with pytest.raises(StrategyError, match=r'This is an error.'):
strategy_safe_wrapper(failing_method, message='DeadBeef')()
assert log_has_re(r'DeadBeef.*', caplog)
ret = strategy_safe_wrapper(failing_method, message='DeadBeef', default_retval=True)()
assert isinstance(ret, bool)
assert ret
@pytest.mark.parametrize('value', [
1, 22, 55, True, False, {'a': 1, 'b': '112'},
[1, 2, 3, 4], (4, 2, 3, 6)
])
def test_strategy_safe_wrapper(value):
def working_method(argumentpassedin):
return argumentpassedin
ret = strategy_safe_wrapper(working_method, message='DeadBeef')(value)
assert type(ret) == type(value)
assert ret == value

View File

@@ -18,7 +18,7 @@ from freqtrade.configuration.config_validation import validate_config_schema
from freqtrade.configuration.deprecated_settings import (
check_conflicting_settings, process_deprecated_setting,
process_temporary_deprecated_settings)
from freqtrade.configuration.load_config import load_config_file
from freqtrade.configuration.load_config import load_config_file, log_config_error_range
from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL
from freqtrade.exceptions import OperationalException
from freqtrade.loggers import _set_loggers, setup_logging
@@ -66,6 +66,30 @@ def test_load_config_file(default_conf, mocker, caplog) -> None:
assert validated_conf.items() >= default_conf.items()
def test_load_config_file_error(default_conf, mocker, caplog) -> None:
del default_conf['user_data_dir']
filedata = json.dumps(default_conf).replace(
'"stake_amount": 0.001,', '"stake_amount": .001,')
mocker.patch('freqtrade.configuration.load_config.open', mocker.mock_open(read_data=filedata))
mocker.patch.object(Path, "read_text", MagicMock(return_value=filedata))
with pytest.raises(OperationalException, match=r".*Please verify the following segment.*"):
load_config_file('somefile')
def test_load_config_file_error_range(default_conf, mocker, caplog) -> None:
del default_conf['user_data_dir']
filedata = json.dumps(default_conf).replace(
'"stake_amount": 0.001,', '"stake_amount": .001,')
mocker.patch.object(Path, "read_text", MagicMock(return_value=filedata))
x = log_config_error_range('somefile', 'Parse error at offset 64: Invalid value.')
assert isinstance(x, str)
assert (x == '{"max_open_trades": 1, "stake_currency": "BTC", '
'"stake_amount": .001, "fiat_display_currency": "USD", '
'"ticker_interval": "5m", "dry_run": true, ')
def test__args_to_config(caplog):
arg_list = ['trade', '--strategy-path', 'TestTest']
@@ -73,6 +97,7 @@ def test__args_to_config(caplog):
configuration = Configuration(args)
config = {}
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
# No warnings ...
configuration._args_to_config(config, argname="strategy_path", logstring="DeadBeef")
assert len(w) == 0
@@ -82,6 +107,7 @@ def test__args_to_config(caplog):
configuration = Configuration(args)
config = {}
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
# Deprecation warnings!
configuration._args_to_config(config, argname="strategy_path", logstring="DeadBeef",
deprecated_msg="Going away soon!")
@@ -1015,18 +1041,6 @@ def test_process_temporary_deprecated_settings(mocker, default_conf, setting, ca
assert default_conf[setting[0]][setting[1]] == setting[5]
def test_process_deprecated_setting_pairlists(mocker, default_conf, caplog):
patched_configuration_load_config_file(mocker, default_conf)
default_conf.update({'pairlist': {
'method': 'VolumePairList',
'config': {'precision_filter': True}
}})
process_temporary_deprecated_settings(default_conf)
assert log_has_re(r'DEPRECATED.*precision_filter.*', caplog)
assert log_has_re(r'DEPRECATED.*in pairlist is deprecated and must be moved*', caplog)
def test_process_deprecated_setting_edge(mocker, edge_conf, caplog):
patched_configuration_load_config_file(mocker, edge_conf)
edge_conf.update({'edge': {

View File

@@ -25,7 +25,7 @@ def test_create_userdata_dir(mocker, default_conf, caplog) -> None:
md = mocker.patch.object(Path, 'mkdir', MagicMock())
x = create_userdata_dir('/tmp/bar', create_dir=True)
assert md.call_count == 8
assert md.call_count == 9
assert md.call_args[1]['parents'] is False
assert log_has(f'Created user-data directory: {Path("/tmp/bar")}', caplog)
assert isinstance(x, Path)

View File

@@ -11,7 +11,7 @@ import arrow
import pytest
import requests
from freqtrade.constants import MATH_CLOSE_PREC, UNLIMITED_STAKE_AMOUNT
from freqtrade.constants import MATH_CLOSE_PREC, UNLIMITED_STAKE_AMOUNT, CANCEL_REASON
from freqtrade.exceptions import (DependencyException, InvalidOrderException,
OperationalException, TemporaryError)
from freqtrade.freqtradebot import FreqtradeBot
@@ -22,7 +22,7 @@ from freqtrade.strategy.interface import SellCheckTuple, SellType
from freqtrade.worker import Worker
from tests.conftest import (get_patched_freqtradebot, get_patched_worker,
log_has, log_has_re, patch_edge, patch_exchange,
patch_get_signal, patch_wallet, patch_whitelist)
patch_get_signal, patch_wallet, patch_whitelist, create_mock_trades)
def patch_RPCManager(mocker) -> MagicMock:
@@ -48,13 +48,31 @@ def test_freqtradebot_state(mocker, default_conf, markets) -> None:
assert freqtrade.state is State.STOPPED
def test_cleanup(mocker, default_conf, caplog) -> None:
mock_cleanup = MagicMock()
mocker.patch('freqtrade.persistence.cleanup', mock_cleanup)
def test_process_stopped(mocker, default_conf) -> None:
freqtrade = get_patched_freqtradebot(mocker, default_conf)
coo_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.cancel_all_open_orders')
freqtrade.process_stopped()
assert coo_mock.call_count == 0
default_conf['cancel_open_orders_on_exit'] = True
freqtrade = get_patched_freqtradebot(mocker, default_conf)
freqtrade.process_stopped()
assert coo_mock.call_count == 1
def test_bot_cleanup(mocker, default_conf, caplog) -> None:
mock_cleanup = mocker.patch('freqtrade.persistence.cleanup')
coo_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.cancel_all_open_orders')
freqtrade = get_patched_freqtradebot(mocker, default_conf)
freqtrade.cleanup()
assert log_has('Cleaning up modules ...', caplog)
assert mock_cleanup.call_count == 1
assert coo_mock.call_count == 0
freqtrade.config['cancel_open_orders_on_exit'] = True
freqtrade.cleanup()
assert coo_mock.call_count == 1
def test_order_dict_dry_run(default_conf, mocker, caplog) -> None:
@@ -1140,7 +1158,8 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog,
'status': 'closed',
'type': 'stop_loss_limit',
'price': 3,
'average': 2
'average': 2,
'amount': limit_buy_order['amount'],
})
mocker.patch('freqtrade.exchange.Exchange.get_order', stoploss_order_hit)
assert freqtrade.handle_stoploss_on_exchange(trade) is True
@@ -1592,13 +1611,13 @@ def test_exit_positions_exception(mocker, default_conf, limit_buy_order, caplog)
mocker.patch('freqtrade.exchange.Exchange.get_order', return_value=limit_buy_order)
trade = MagicMock()
trade.open_order_id = '123'
trade.open_order_id = None
trade.open_fee = 0.001
trades = [trade]
# Test raise of DependencyException exception
mocker.patch(
'freqtrade.freqtradebot.FreqtradeBot.update_trade_state',
'freqtrade.freqtradebot.FreqtradeBot.handle_trade',
side_effect=DependencyException()
)
n = freqtrade.exit_positions(trades)
@@ -1939,8 +1958,10 @@ def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order,
freqtrade.handle_trade(trade)
def test_check_handle_timedout_buy(default_conf, ticker, limit_buy_order_old, open_trade,
fee, mocker) -> None:
def test_check_handle_timedout_buy_usercustom(default_conf, ticker, limit_buy_order_old, open_trade,
fee, mocker) -> None:
default_conf["unfilledtimeout"] = {"buy": 1400, "sell": 30}
rpc_mock = patch_RPCManager(mocker)
cancel_order_mock = MagicMock(return_value=limit_buy_order_old)
patch_exchange(mocker)
@@ -1955,6 +1976,56 @@ def test_check_handle_timedout_buy(default_conf, ticker, limit_buy_order_old, op
Trade.session.add(open_trade)
# Ensure default is to return empty (so not mocked yet)
freqtrade.check_handle_timedout()
assert cancel_order_mock.call_count == 0
# Return false - trade remains open
freqtrade.strategy.check_buy_timeout = MagicMock(return_value=False)
freqtrade.check_handle_timedout()
assert cancel_order_mock.call_count == 0
trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all()
nb_trades = len(trades)
assert nb_trades == 1
assert freqtrade.strategy.check_buy_timeout.call_count == 1
# Raise Keyerror ... (no impact on trade)
freqtrade.strategy.check_buy_timeout = MagicMock(side_effect=KeyError)
freqtrade.check_handle_timedout()
assert cancel_order_mock.call_count == 0
trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all()
nb_trades = len(trades)
assert nb_trades == 1
assert freqtrade.strategy.check_buy_timeout.call_count == 1
freqtrade.strategy.check_buy_timeout = MagicMock(return_value=True)
# Trade should be closed since the function returns true
freqtrade.check_handle_timedout()
assert cancel_order_mock.call_count == 1
assert rpc_mock.call_count == 1
trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all()
nb_trades = len(trades)
assert nb_trades == 0
assert freqtrade.strategy.check_buy_timeout.call_count == 1
def test_check_handle_timedout_buy(default_conf, ticker, limit_buy_order_old, open_trade,
fee, mocker) -> None:
rpc_mock = patch_RPCManager(mocker)
cancel_order_mock = MagicMock(return_value=limit_buy_order_old)
patch_exchange(mocker)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
fetch_ticker=ticker,
get_order=MagicMock(return_value=limit_buy_order_old),
cancel_order_with_result=cancel_order_mock,
get_fee=fee
)
freqtrade = FreqtradeBot(default_conf)
Trade.session.add(open_trade)
freqtrade.strategy.check_buy_timeout = MagicMock(return_value=False)
# check it does cancel buy orders over the time limit
freqtrade.check_handle_timedout()
assert cancel_order_mock.call_count == 1
@@ -1962,6 +2033,8 @@ def test_check_handle_timedout_buy(default_conf, ticker, limit_buy_order_old, op
trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all()
nb_trades = len(trades)
assert nb_trades == 0
# Custom user buy-timeout is never called
assert freqtrade.strategy.check_buy_timeout.call_count == 0
def test_check_handle_cancelled_buy(default_conf, ticker, limit_buy_order_old, open_trade,
@@ -1970,7 +2043,7 @@ def test_check_handle_cancelled_buy(default_conf, ticker, limit_buy_order_old, o
rpc_mock = patch_RPCManager(mocker)
cancel_order_mock = MagicMock()
patch_exchange(mocker)
limit_buy_order_old.update({"status": "canceled"})
limit_buy_order_old.update({"status": "canceled", 'filled': 0.0})
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
fetch_ticker=ticker,
@@ -2018,6 +2091,54 @@ def test_check_handle_timedout_buy_exception(default_conf, ticker, limit_buy_ord
assert nb_trades == 1
def test_check_handle_timedout_sell_usercustom(default_conf, ticker, limit_sell_order_old, mocker,
open_trade) -> None:
default_conf["unfilledtimeout"] = {"buy": 1440, "sell": 1440}
rpc_mock = patch_RPCManager(mocker)
cancel_order_mock = MagicMock()
patch_exchange(mocker)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
fetch_ticker=ticker,
get_order=MagicMock(return_value=limit_sell_order_old),
cancel_order=cancel_order_mock
)
freqtrade = FreqtradeBot(default_conf)
open_trade.open_date = arrow.utcnow().shift(hours=-5).datetime
open_trade.close_date = arrow.utcnow().shift(minutes=-601).datetime
open_trade.is_open = False
Trade.session.add(open_trade)
# Ensure default is false
freqtrade.check_handle_timedout()
assert cancel_order_mock.call_count == 0
freqtrade.strategy.check_sell_timeout = MagicMock(return_value=False)
# Return false - No impact
freqtrade.check_handle_timedout()
assert cancel_order_mock.call_count == 0
assert rpc_mock.call_count == 0
assert open_trade.is_open is False
assert freqtrade.strategy.check_sell_timeout.call_count == 1
freqtrade.strategy.check_sell_timeout = MagicMock(side_effect=KeyError)
# Return Error - No impact
freqtrade.check_handle_timedout()
assert cancel_order_mock.call_count == 0
assert rpc_mock.call_count == 0
assert open_trade.is_open is False
assert freqtrade.strategy.check_sell_timeout.call_count == 1
# Return True - sells!
freqtrade.strategy.check_sell_timeout = MagicMock(return_value=True)
freqtrade.check_handle_timedout()
assert cancel_order_mock.call_count == 1
assert rpc_mock.call_count == 1
assert open_trade.is_open is True
assert freqtrade.strategy.check_sell_timeout.call_count == 1
def test_check_handle_timedout_sell(default_conf, ticker, limit_sell_order_old, mocker,
open_trade) -> None:
rpc_mock = patch_RPCManager(mocker)
@@ -2037,11 +2158,14 @@ def test_check_handle_timedout_sell(default_conf, ticker, limit_sell_order_old,
Trade.session.add(open_trade)
freqtrade.strategy.check_sell_timeout = MagicMock(return_value=False)
# check it does cancel sell orders over the time limit
freqtrade.check_handle_timedout()
assert cancel_order_mock.call_count == 1
assert rpc_mock.call_count == 1
assert open_trade.is_open is True
# Custom user sell-timeout is never called
assert freqtrade.strategy.check_sell_timeout.call_count == 0
def test_check_handle_cancelled_sell(default_conf, ticker, limit_sell_order_old, open_trade,
@@ -2049,13 +2173,13 @@ def test_check_handle_cancelled_sell(default_conf, ticker, limit_sell_order_old,
""" Handle sell order cancelled on exchange"""
rpc_mock = patch_RPCManager(mocker)
cancel_order_mock = MagicMock()
limit_sell_order_old.update({"status": "canceled"})
limit_sell_order_old.update({"status": "canceled", 'filled': 0.0})
patch_exchange(mocker)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
fetch_ticker=ticker,
get_order=MagicMock(return_value=limit_sell_order_old),
cancel_order=cancel_order_mock
cancel_order_with_result=cancel_order_mock
)
freqtrade = FreqtradeBot(default_conf)
@@ -2082,7 +2206,7 @@ def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old
'freqtrade.exchange.Exchange',
fetch_ticker=ticker,
get_order=MagicMock(return_value=limit_buy_order_old_partial),
cancel_order=cancel_order_mock
cancel_order_with_result=cancel_order_mock
)
freqtrade = FreqtradeBot(default_conf)
@@ -2104,12 +2228,13 @@ def test_check_handle_timedout_partial_fee(default_conf, ticker, open_trade, cap
limit_buy_order_old_partial_canceled, mocker) -> None:
rpc_mock = patch_RPCManager(mocker)
cancel_order_mock = MagicMock(return_value=limit_buy_order_old_partial_canceled)
mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=0))
patch_exchange(mocker)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
fetch_ticker=ticker,
get_order=MagicMock(return_value=limit_buy_order_old_partial),
cancel_order=cancel_order_mock,
cancel_order_with_result=cancel_order_mock,
get_trades_for_order=MagicMock(return_value=trades_for_order),
)
freqtrade = FreqtradeBot(default_conf)
@@ -2123,17 +2248,18 @@ def test_check_handle_timedout_partial_fee(default_conf, ticker, open_trade, cap
# and apply fees if necessary.
freqtrade.check_handle_timedout()
assert log_has_re(r"Applying fee on amount for Trade.* Order", caplog)
assert log_has_re(r"Applying fee on amount for Trade.*", caplog)
assert cancel_order_mock.call_count == 1
assert rpc_mock.call_count == 2
trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all()
assert len(trades) == 1
# Verify that tradehas been updated
# Verify that trade has been updated
assert trades[0].amount == (limit_buy_order_old_partial['amount'] -
limit_buy_order_old_partial['remaining']) - 0.0001
limit_buy_order_old_partial['remaining']) - 0.023
assert trades[0].open_order_id is None
assert trades[0].fee_open == 0
assert trades[0].fee_updated('buy')
assert pytest.approx(trades[0].fee_open) == 0.001
def test_check_handle_timedout_partial_except(default_conf, ticker, open_trade, caplog, fee,
@@ -2146,7 +2272,7 @@ def test_check_handle_timedout_partial_except(default_conf, ticker, open_trade,
'freqtrade.exchange.Exchange',
fetch_ticker=ticker,
get_order=MagicMock(return_value=limit_buy_order_old_partial),
cancel_order=cancel_order_mock,
cancel_order_with_result=cancel_order_mock,
get_trades_for_order=MagicMock(return_value=trades_for_order),
)
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount',
@@ -2168,7 +2294,7 @@ def test_check_handle_timedout_partial_except(default_conf, ticker, open_trade,
assert rpc_mock.call_count == 2
trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all()
assert len(trades) == 1
# Verify that tradehas been updated
# Verify that trade has been updated
assert trades[0].amount == (limit_buy_order_old_partial['amount'] -
limit_buy_order_old_partial['remaining'])
@@ -2183,8 +2309,8 @@ def test_check_handle_timedout_exception(default_conf, ticker, open_trade, mocke
mocker.patch.multiple(
'freqtrade.freqtradebot.FreqtradeBot',
handle_timedout_limit_buy=MagicMock(),
handle_timedout_limit_sell=MagicMock(),
handle_cancel_buy=MagicMock(),
handle_cancel_sell=MagicMock(),
)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
@@ -2204,75 +2330,147 @@ def test_check_handle_timedout_exception(default_conf, ticker, open_trade, mocke
caplog)
def test_handle_timedout_limit_buy(mocker, default_conf, limit_buy_order) -> None:
def test_handle_cancel_buy(mocker, caplog, default_conf, limit_buy_order) -> None:
patch_RPCManager(mocker)
patch_exchange(mocker)
cancel_order_mock = MagicMock(return_value=limit_buy_order)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
cancel_order=cancel_order_mock
)
mocker.patch('freqtrade.exchange.Exchange.cancel_order_with_result', cancel_order_mock)
freqtrade = FreqtradeBot(default_conf)
freqtrade._notify_buy_cancel = MagicMock()
Trade.session = MagicMock()
trade = MagicMock()
trade.pair = 'LTC/ETH'
limit_buy_order['remaining'] = limit_buy_order['amount']
assert freqtrade.handle_timedout_limit_buy(trade, limit_buy_order)
limit_buy_order['filled'] = 0.0
limit_buy_order['status'] = 'open'
reason = CANCEL_REASON['TIMEOUT']
assert freqtrade.handle_cancel_buy(trade, limit_buy_order, reason)
assert cancel_order_mock.call_count == 1
cancel_order_mock.reset_mock()
limit_buy_order['amount'] = 2
assert not freqtrade.handle_timedout_limit_buy(trade, limit_buy_order)
limit_buy_order['filled'] = 2
assert not freqtrade.handle_cancel_buy(trade, limit_buy_order, reason)
assert cancel_order_mock.call_count == 1
limit_buy_order['filled'] = 2
mocker.patch('freqtrade.exchange.Exchange.cancel_order', side_effect=InvalidOrderException)
assert not freqtrade.handle_cancel_buy(trade, limit_buy_order, reason)
def test_handle_timedout_limit_buy_corder_empty(mocker, default_conf, limit_buy_order) -> None:
@pytest.mark.parametrize("limit_buy_order_canceled_empty", ['binance', 'ftx', 'kraken', 'bittrex'],
indirect=['limit_buy_order_canceled_empty'])
def test_handle_cancel_buy_exchanges(mocker, caplog, default_conf,
limit_buy_order_canceled_empty) -> None:
patch_RPCManager(mocker)
patch_exchange(mocker)
cancel_order_mock = MagicMock(return_value={})
cancel_order_mock = mocker.patch(
'freqtrade.exchange.Exchange.cancel_order_with_result',
return_value=limit_buy_order_canceled_empty)
nofiy_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot._notify_buy_cancel')
freqtrade = FreqtradeBot(default_conf)
Trade.session = MagicMock()
reason = CANCEL_REASON['TIMEOUT']
trade = MagicMock()
trade.pair = 'LTC/ETH'
assert freqtrade.handle_cancel_buy(trade, limit_buy_order_canceled_empty, reason)
assert cancel_order_mock.call_count == 0
assert log_has_re(r'Buy order fully cancelled. Removing .* from database\.', caplog)
assert nofiy_mock.call_count == 1
@pytest.mark.parametrize('cancelorder', [
{},
{'remaining': None},
'String Return value',
123
])
def test_handle_cancel_buy_corder_empty(mocker, default_conf, limit_buy_order,
cancelorder) -> None:
patch_RPCManager(mocker)
patch_exchange(mocker)
cancel_order_mock = MagicMock(return_value=cancelorder)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
cancel_order=cancel_order_mock
)
freqtrade = FreqtradeBot(default_conf)
freqtrade._notify_buy_cancel = MagicMock()
Trade.session = MagicMock()
trade = MagicMock()
trade.pair = 'LTC/ETH'
limit_buy_order['remaining'] = limit_buy_order['amount']
assert freqtrade.handle_timedout_limit_buy(trade, limit_buy_order)
limit_buy_order['filled'] = 0.0
limit_buy_order['status'] = 'open'
reason = CANCEL_REASON['TIMEOUT']
assert freqtrade.handle_cancel_buy(trade, limit_buy_order, reason)
assert cancel_order_mock.call_count == 1
cancel_order_mock.reset_mock()
limit_buy_order['amount'] = 2
assert not freqtrade.handle_timedout_limit_buy(trade, limit_buy_order)
limit_buy_order['filled'] = 1.0
assert not freqtrade.handle_cancel_buy(trade, limit_buy_order, reason)
assert cancel_order_mock.call_count == 1
def test_handle_timedout_limit_sell(mocker, default_conf) -> None:
patch_RPCManager(mocker)
def test_handle_cancel_sell_limit(mocker, default_conf, fee) -> None:
send_msg_mock = patch_RPCManager(mocker)
patch_exchange(mocker)
cancel_order_mock = MagicMock()
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
cancel_order=cancel_order_mock
cancel_order=cancel_order_mock,
)
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate', return_value=0.245441)
freqtrade = FreqtradeBot(default_conf)
trade = Trade(
pair='LTC/ETH',
amount=2,
exchange='binance',
open_rate=0.245441,
open_order_id="123456",
open_date=arrow.utcnow().datetime,
fee_open=fee.return_value,
fee_close=fee.return_value,
)
order = {'remaining': 1,
'amount': 1,
'status': "open"}
reason = CANCEL_REASON['TIMEOUT']
assert freqtrade.handle_cancel_sell(trade, order, reason)
assert cancel_order_mock.call_count == 1
assert send_msg_mock.call_count == 1
send_msg_mock.reset_mock()
order['amount'] = 2
assert freqtrade.handle_cancel_sell(trade, order, reason) == CANCEL_REASON['PARTIALLY_FILLED']
# Assert cancel_order was not called (callcount remains unchanged)
assert cancel_order_mock.call_count == 1
assert send_msg_mock.call_count == 1
assert freqtrade.handle_cancel_sell(trade, order, reason) == CANCEL_REASON['PARTIALLY_FILLED']
# Message should not be iterated again
assert trade.sell_order_status == CANCEL_REASON['PARTIALLY_FILLED']
assert send_msg_mock.call_count == 1
def test_handle_cancel_sell_cancel_exception(mocker, default_conf) -> None:
patch_RPCManager(mocker)
patch_exchange(mocker)
mocker.patch(
'freqtrade.exchange.Exchange.cancel_order', side_effect=InvalidOrderException())
freqtrade = FreqtradeBot(default_conf)
trade = MagicMock()
reason = CANCEL_REASON['TIMEOUT']
order = {'remaining': 1,
'amount': 1,
'status': "open"}
assert freqtrade.handle_timedout_limit_sell(trade, order)
assert cancel_order_mock.call_count == 1
order['amount'] = 2
assert not freqtrade.handle_timedout_limit_sell(trade, order)
# Assert cancel_order was not called (callcount remains unchanged)
assert cancel_order_mock.call_count == 1
assert freqtrade.handle_cancel_sell(trade, order, reason) == 'error cancelling order'
def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> None:
@@ -2493,6 +2691,7 @@ def test_execute_sell_with_stoploss_on_exchange(default_conf, ticker, fee, ticke
assert trade
trades = [trade]
freqtrade.check_handle_timedout()
freqtrade.exit_positions(trades)
# Increase the price and sell it
@@ -2538,8 +2737,11 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, ticker, f
# Create some test data
freqtrade.enter_positions()
freqtrade.check_handle_timedout()
trade = Trade.query.first()
trades = [trade]
assert trade.stoploss_order_id is None
freqtrade.exit_positions(trades)
assert trade
assert trade.stoploss_order_id == '123'
@@ -2951,10 +3153,8 @@ def test_trailing_stop_loss(default_conf, limit_buy_order, fee, caplog, mocker)
caplog.set_level(logging.DEBUG)
# Sell as trailing-stop is reached
assert freqtrade.handle_trade(trade) is True
assert log_has(
f"ETH/BTC - HIT STOP: current price at 0.000012, "
f"stoploss is 0.000015, "
f"initial stoploss was at 0.000010, trade opened at 0.000011", caplog)
assert log_has("ETH/BTC - HIT STOP: current price at 0.000012, stoploss is 0.000015, "
"initial stoploss was at 0.000010, trade opened at 0.000011", caplog)
assert trade.sell_reason == SellType.TRAILING_STOP_LOSS.value
@@ -2997,8 +3197,8 @@ def test_trailing_stop_loss_positive(default_conf, limit_buy_order, fee,
}))
# stop-loss not reached, adjusted stoploss
assert freqtrade.handle_trade(trade) is False
assert log_has(f"ETH/BTC - Using positive stoploss: 0.01 offset: 0 profit: 0.2666%", caplog)
assert log_has(f"ETH/BTC - Adjusting stoploss...", caplog)
assert log_has("ETH/BTC - Using positive stoploss: 0.01 offset: 0 profit: 0.2666%", caplog)
assert log_has("ETH/BTC - Adjusting stoploss...", caplog)
assert trade.stop_loss == 0.0000138501
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker',
@@ -3054,9 +3254,8 @@ def test_trailing_stop_loss_offset(default_conf, limit_buy_order, fee,
}))
# stop-loss not reached, adjusted stoploss
assert freqtrade.handle_trade(trade) is False
assert log_has(f"ETH/BTC - Using positive stoploss: 0.01 offset: 0.011 profit: 0.2666%",
caplog)
assert log_has(f"ETH/BTC - Adjusting stoploss...", caplog)
assert log_has("ETH/BTC - Using positive stoploss: 0.01 offset: 0.011 profit: 0.2666%", caplog)
assert log_has("ETH/BTC - Adjusting stoploss...", caplog)
assert trade.stop_loss == 0.0000138501
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker',
@@ -3120,7 +3319,7 @@ def test_tsl_only_offset_reached(default_conf, limit_buy_order, fee,
# stop-loss should not be adjusted as offset is not reached yet
assert freqtrade.handle_trade(trade) is False
assert not log_has(f"ETH/BTC - Adjusting stoploss...", caplog)
assert not log_has("ETH/BTC - Adjusting stoploss...", caplog)
assert trade.stop_loss == 0.0000098910
# price rises above the offset (rises 12% when the offset is 5.5%)
@@ -3132,9 +3331,8 @@ def test_tsl_only_offset_reached(default_conf, limit_buy_order, fee,
}))
assert freqtrade.handle_trade(trade) is False
assert log_has(f"ETH/BTC - Using positive stoploss: 0.05 offset: 0.055 profit: 0.1218%",
caplog)
assert log_has(f"ETH/BTC - Adjusting stoploss...", caplog)
assert log_has("ETH/BTC - Using positive stoploss: 0.05 offset: 0.055 profit: 0.1218%", caplog)
assert log_has("ETH/BTC - Adjusting stoploss...", caplog)
assert trade.stop_loss == 0.0000117705
@@ -3175,8 +3373,6 @@ def test_disable_ignore_roi_if_buy_signal(default_conf, limit_buy_order,
def test_get_real_amount_quote(default_conf, trades_for_order, buy_order_fee, fee, caplog, mocker):
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order)
patch_RPCManager(mocker)
patch_exchange(mocker)
amount = sum(x['amount'] for x in trades_for_order)
trade = Trade(
pair='LTC/ETH',
@@ -3187,21 +3383,43 @@ def test_get_real_amount_quote(default_conf, trades_for_order, buy_order_fee, fe
fee_close=fee.return_value,
open_order_id="123456"
)
freqtrade = FreqtradeBot(default_conf)
patch_get_signal(freqtrade)
freqtrade = get_patched_freqtradebot(mocker, default_conf)
# Amount is reduced by "fee"
assert freqtrade.get_real_amount(trade, buy_order_fee) == amount - (amount * 0.001)
assert log_has('Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, '
'open_rate=0.24544100, open_since=closed) (from 8.0 to 7.992) from Trades',
'open_rate=0.24544100, open_since=closed) (from 8.0 to 7.992).',
caplog)
def test_get_real_amount_quote_dust(default_conf, trades_for_order, buy_order_fee, fee,
caplog, mocker):
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order)
walletmock = mocker.patch('freqtrade.wallets.Wallets.update')
mocker.patch('freqtrade.wallets.Wallets.get_free', return_value=8.1122)
amount = sum(x['amount'] for x in trades_for_order)
trade = Trade(
pair='LTC/ETH',
amount=amount,
exchange='binance',
open_rate=0.245441,
fee_open=fee.return_value,
fee_close=fee.return_value,
open_order_id="123456"
)
freqtrade = get_patched_freqtradebot(mocker, default_conf)
walletmock.reset_mock()
# Amount is kept as is
assert freqtrade.get_real_amount(trade, buy_order_fee) == amount
assert walletmock.call_count == 1
assert log_has_re(r'Fee amount for Trade.* was in base currency '
'- Eating Fee 0.008 into dust', caplog)
def test_get_real_amount_no_trade(default_conf, buy_order_fee, caplog, mocker, fee):
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[])
patch_RPCManager(mocker)
patch_exchange(mocker)
amount = buy_order_fee['amount']
trade = Trade(
pair='LTC/ETH',
@@ -3212,8 +3430,7 @@ def test_get_real_amount_no_trade(default_conf, buy_order_fee, caplog, mocker, f
fee_close=fee.return_value,
open_order_id="123456"
)
freqtrade = FreqtradeBot(default_conf)
patch_get_signal(freqtrade)
freqtrade = get_patched_freqtradebot(mocker, default_conf)
# Amount is reduced by "fee"
assert freqtrade.get_real_amount(trade, buy_order_fee) == amount
@@ -3225,8 +3442,6 @@ def test_get_real_amount_no_trade(default_conf, buy_order_fee, caplog, mocker, f
def test_get_real_amount_stake(default_conf, trades_for_order, buy_order_fee, fee, mocker):
trades_for_order[0]['fee']['currency'] = 'ETH'
patch_RPCManager(mocker)
patch_exchange(mocker)
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order)
amount = sum(x['amount'] for x in trades_for_order)
trade = Trade(
@@ -3238,8 +3453,7 @@ def test_get_real_amount_stake(default_conf, trades_for_order, buy_order_fee, fe
open_rate=0.245441,
open_order_id="123456"
)
freqtrade = FreqtradeBot(default_conf)
patch_get_signal(freqtrade)
freqtrade = get_patched_freqtradebot(mocker, default_conf)
# Amount does not change
assert freqtrade.get_real_amount(trade, buy_order_fee) == amount
@@ -3252,8 +3466,6 @@ def test_get_real_amount_no_currency_in_fee(default_conf, trades_for_order, buy_
limit_buy_order['fee'] = {'cost': 0.004, 'currency': None}
trades_for_order[0]['fee']['currency'] = None
patch_RPCManager(mocker)
patch_exchange(mocker)
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order)
amount = sum(x['amount'] for x in trades_for_order)
trade = Trade(
@@ -3265,8 +3477,7 @@ def test_get_real_amount_no_currency_in_fee(default_conf, trades_for_order, buy_
open_rate=0.245441,
open_order_id="123456"
)
freqtrade = FreqtradeBot(default_conf)
patch_get_signal(freqtrade)
freqtrade = get_patched_freqtradebot(mocker, default_conf)
# Amount does not change
assert freqtrade.get_real_amount(trade, limit_buy_order) == amount
@@ -3276,8 +3487,6 @@ def test_get_real_amount_BNB(default_conf, trades_for_order, buy_order_fee, fee,
trades_for_order[0]['fee']['currency'] = 'BNB'
trades_for_order[0]['fee']['cost'] = 0.00094518
patch_RPCManager(mocker)
patch_exchange(mocker)
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order)
amount = sum(x['amount'] for x in trades_for_order)
trade = Trade(
@@ -3289,16 +3498,13 @@ def test_get_real_amount_BNB(default_conf, trades_for_order, buy_order_fee, fee,
open_rate=0.245441,
open_order_id="123456"
)
freqtrade = FreqtradeBot(default_conf)
patch_get_signal(freqtrade)
freqtrade = get_patched_freqtradebot(mocker, default_conf)
# Amount does not change
assert freqtrade.get_real_amount(trade, buy_order_fee) == amount
def test_get_real_amount_multi(default_conf, trades_for_order2, buy_order_fee, caplog, fee, mocker):
patch_RPCManager(mocker)
patch_exchange(mocker)
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order2)
amount = float(sum(x['amount'] for x in trades_for_order2))
trade = Trade(
@@ -3310,13 +3516,12 @@ def test_get_real_amount_multi(default_conf, trades_for_order2, buy_order_fee, c
open_rate=0.245441,
open_order_id="123456"
)
freqtrade = FreqtradeBot(default_conf)
patch_get_signal(freqtrade)
freqtrade = get_patched_freqtradebot(mocker, default_conf)
# Amount is reduced by "fee"
assert freqtrade.get_real_amount(trade, buy_order_fee) == amount - (amount * 0.001)
assert log_has('Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, '
'open_rate=0.24544100, open_since=closed) (from 8.0 to 7.992) from Trades',
'open_rate=0.24544100, open_since=closed) (from 8.0 to 7.992).',
caplog)
@@ -3325,8 +3530,6 @@ def test_get_real_amount_fromorder(default_conf, trades_for_order, buy_order_fee
limit_buy_order = deepcopy(buy_order_fee)
limit_buy_order['fee'] = {'cost': 0.004, 'currency': 'LTC'}
patch_RPCManager(mocker)
patch_exchange(mocker)
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order',
return_value=[trades_for_order])
amount = float(sum(x['amount'] for x in trades_for_order))
@@ -3339,13 +3542,12 @@ def test_get_real_amount_fromorder(default_conf, trades_for_order, buy_order_fee
open_rate=0.245441,
open_order_id="123456"
)
freqtrade = FreqtradeBot(default_conf)
patch_get_signal(freqtrade)
freqtrade = get_patched_freqtradebot(mocker, default_conf)
# Amount is reduced by "fee"
assert freqtrade.get_real_amount(trade, limit_buy_order) == amount - 0.004
assert log_has('Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, '
'open_rate=0.24544100, open_since=closed) (from 8.0 to 7.996) from Order',
'open_rate=0.24544100, open_since=closed) (from 8.0 to 7.996).',
caplog)
@@ -3353,8 +3555,6 @@ def test_get_real_amount_invalid_order(default_conf, trades_for_order, buy_order
limit_buy_order = deepcopy(buy_order_fee)
limit_buy_order['fee'] = {'cost': 0.004}
patch_RPCManager(mocker)
patch_exchange(mocker)
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[])
amount = float(sum(x['amount'] for x in trades_for_order))
trade = Trade(
@@ -3366,8 +3566,7 @@ def test_get_real_amount_invalid_order(default_conf, trades_for_order, buy_order
open_rate=0.245441,
open_order_id="123456"
)
freqtrade = FreqtradeBot(default_conf)
patch_get_signal(freqtrade)
freqtrade = get_patched_freqtradebot(mocker, default_conf)
# Amount does not change
assert freqtrade.get_real_amount(trade, limit_buy_order) == amount
@@ -3377,8 +3576,6 @@ def test_get_real_amount_wrong_amount(default_conf, trades_for_order, buy_order_
limit_buy_order = deepcopy(buy_order_fee)
limit_buy_order['amount'] = limit_buy_order['amount'] - 0.001
patch_RPCManager(mocker)
patch_exchange(mocker)
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order)
amount = float(sum(x['amount'] for x in trades_for_order))
trade = Trade(
@@ -3390,8 +3587,7 @@ def test_get_real_amount_wrong_amount(default_conf, trades_for_order, buy_order_
fee_close=fee.return_value,
open_order_id="123456"
)
freqtrade = FreqtradeBot(default_conf)
patch_get_signal(freqtrade)
freqtrade = get_patched_freqtradebot(mocker, default_conf)
# Amount does not change
with pytest.raises(DependencyException, match=r"Half bought\? Amounts don't match"):
@@ -3404,8 +3600,6 @@ def test_get_real_amount_wrong_amount_rounding(default_conf, trades_for_order, b
limit_buy_order = deepcopy(buy_order_fee)
trades_for_order[0]['amount'] = trades_for_order[0]['amount'] + 1e-15
patch_RPCManager(mocker)
patch_exchange(mocker)
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order)
amount = float(sum(x['amount'] for x in trades_for_order))
trade = Trade(
@@ -3417,8 +3611,7 @@ def test_get_real_amount_wrong_amount_rounding(default_conf, trades_for_order, b
open_rate=0.245441,
open_order_id="123456"
)
freqtrade = FreqtradeBot(default_conf)
patch_get_signal(freqtrade)
freqtrade = get_patched_freqtradebot(mocker, default_conf)
# Amount changes by fee amount.
assert isclose(freqtrade.get_real_amount(trade, limit_buy_order), amount - (amount * 0.001),
@@ -3429,8 +3622,6 @@ def test_get_real_amount_invalid(default_conf, trades_for_order, buy_order_fee,
# Remove "Currency" from fee dict
trades_for_order[0]['fee'] = {'cost': 0.008}
patch_RPCManager(mocker)
patch_exchange(mocker)
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order)
amount = sum(x['amount'] for x in trades_for_order)
trade = Trade(
@@ -3443,15 +3634,12 @@ def test_get_real_amount_invalid(default_conf, trades_for_order, buy_order_fee,
open_order_id="123456"
)
freqtrade = FreqtradeBot(default_conf)
patch_get_signal(freqtrade)
freqtrade = get_patched_freqtradebot(mocker, default_conf)
# Amount does not change
assert freqtrade.get_real_amount(trade, buy_order_fee) == amount
def test_get_real_amount_open_trade(default_conf, fee, mocker):
patch_RPCManager(mocker)
patch_exchange(mocker)
amount = 12345
trade = Trade(
pair='LTC/ETH',
@@ -3466,12 +3654,41 @@ def test_get_real_amount_open_trade(default_conf, fee, mocker):
'id': 'mocked_order',
'amount': amount,
'status': 'open',
'side': 'buy',
}
freqtrade = FreqtradeBot(default_conf)
patch_get_signal(freqtrade)
freqtrade = get_patched_freqtradebot(mocker, default_conf)
assert freqtrade.get_real_amount(trade, order) == amount
@pytest.mark.parametrize('amount,fee_abs,wallet,amount_exp', [
(8.0, 0.0, 10, 8),
(8.0, 0.0, 0, 8),
(8.0, 0.1, 0, 7.9),
(8.0, 0.1, 10, 8),
(8.0, 0.1, 8.0, 8.0),
(8.0, 0.1, 7.9, 7.9),
])
def test_apply_fee_conditional(default_conf, fee, caplog, mocker,
amount, fee_abs, wallet, amount_exp):
walletmock = mocker.patch('freqtrade.wallets.Wallets.update')
mocker.patch('freqtrade.wallets.Wallets.get_free', return_value=wallet)
trade = Trade(
pair='LTC/ETH',
amount=amount,
exchange='binance',
open_rate=0.245441,
fee_open=fee.return_value,
fee_close=fee.return_value,
open_order_id="123456"
)
freqtrade = get_patched_freqtradebot(mocker, default_conf)
walletmock.reset_mock()
# Amount is kept as is
assert freqtrade.apply_fee_conditional(trade, 'LTC', amount, fee_abs) == amount_exp
assert walletmock.call_count == 1
def test_order_book_depth_of_market(default_conf, ticker, limit_buy_order, fee, mocker,
order_book_l2):
default_conf['bid_strategy']['check_depth_of_market']['enabled'] = True
@@ -3748,3 +3965,20 @@ def test_sync_wallet_dry_run(mocker, default_conf, ticker, fee, limit_buy_order,
assert log_has_re(r"Unable to create trade for XRP/BTC: "
r"Available balance \(0.0 BTC\) is lower than stake amount \(0.001 BTC\)",
caplog)
@pytest.mark.usefixtures("init_persistence")
def test_cancel_all_open_orders(mocker, default_conf, fee, limit_buy_order, limit_sell_order):
default_conf['cancel_open_orders_on_exit'] = True
mocker.patch('freqtrade.exchange.Exchange.get_order',
side_effect=[DependencyException(), limit_sell_order, limit_buy_order])
buy_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_buy')
sell_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_sell')
freqtrade = get_patched_freqtradebot(mocker, default_conf)
create_mock_trades(fee)
trades = Trade.query.all()
assert len(trades) == 3
freqtrade.cancel_all_open_orders()
assert buy_mock.call_count == 1
assert sell_mock.call_count == 1

View File

@@ -44,6 +44,8 @@ def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee,
}
stoploss_order_closed = stoploss_order_open.copy()
stoploss_order_closed['status'] = 'closed'
stoploss_order_closed['filled'] = stoploss_order_closed['amount']
# Sell first trade based on stoploss, keep 2nd and 3rd trade open
stoploss_order_mock = MagicMock(
side_effect=[stoploss_order_closed, stoploss_order_open, stoploss_order_open])
@@ -67,7 +69,6 @@ def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee,
mocker.patch.multiple(
'freqtrade.freqtradebot.FreqtradeBot',
create_stoploss_order=MagicMock(return_value=True),
update_trade_state=MagicMock(),
_notify_sell=MagicMock(),
)
mocker.patch("freqtrade.strategy.interface.IStrategy.should_sell", should_sell_mock)
@@ -97,8 +98,9 @@ def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee,
# Only order for 3rd trade needs to be cancelled
assert cancel_order_mock.call_count == 1
# Wallets must be updated between stoploss cancellation and selling.
assert wallets_mock.call_count == 2
# Wallets must be updated between stoploss cancellation and selling, and will be updated again
# during update_trade_state
assert wallets_mock.call_count == 4
trade = trades[0]
assert trade.sell_reason == SellType.STOPLOSS_ON_EXCHANGE.value
@@ -144,7 +146,6 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, limit_buy_order, moc
mocker.patch.multiple(
'freqtrade.freqtradebot.FreqtradeBot',
create_stoploss_order=MagicMock(return_value=True),
update_trade_state=MagicMock(),
_notify_sell=MagicMock(),
)
should_sell_mock = MagicMock(side_effect=[

View File

@@ -115,6 +115,32 @@ def test_main_operational_exception(mocker, default_conf, caplog) -> None:
assert log_has('Oh snap!', caplog)
def test_main_operational_exception1(mocker, default_conf, caplog) -> None:
patch_exchange(mocker)
mocker.patch(
'freqtrade.commands.list_commands.available_exchanges',
MagicMock(side_effect=ValueError('Oh snap!'))
)
patched_configuration_load_config_file(mocker, default_conf)
args = ['list-exchanges']
# Test Main + the KeyboardInterrupt exception
with pytest.raises(SystemExit):
main(args)
assert log_has('Fatal exception!', caplog)
assert not log_has_re(r'SIGINT.*', caplog)
mocker.patch(
'freqtrade.commands.list_commands.available_exchanges',
MagicMock(side_effect=KeyboardInterrupt)
)
with pytest.raises(SystemExit):
main(args)
assert log_has_re(r'SIGINT.*', caplog)
def test_main_reload_conf(mocker, default_conf, caplog) -> None:
patch_exchange(mocker)
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.cleanup', MagicMock())

View File

@@ -6,10 +6,12 @@ from unittest.mock import MagicMock
import pytest
from freqtrade.data.converter import parse_ticker_dataframe
from freqtrade.data.converter import ohlcv_to_dataframe
from freqtrade.misc import (datesarray_to_datetimearray, file_dump_json,
file_load_json, format_ms_time, pair_to_filename,
plural, shorten_date)
plural, render_template,
render_template_with_fallback, safe_value_fallback,
shorten_date)
def test_shorten_date() -> None:
@@ -18,9 +20,9 @@ def test_shorten_date() -> None:
assert shorten_date(str_data) == str_shorten_data
def test_datesarray_to_datetimearray(ticker_history_list):
dataframes = parse_ticker_dataframe(ticker_history_list, "5m", pair="UNITTEST/BTC",
fill_missing=True)
def test_datesarray_to_datetimearray(ohlcv_history_list):
dataframes = ohlcv_to_dataframe(ohlcv_history_list, "5m", pair="UNITTEST/BTC",
fill_missing=True)
dates = datesarray_to_datetimearray(dataframes['date'])
assert isinstance(dates[0], datetime.datetime)
@@ -93,6 +95,27 @@ def test_format_ms_time() -> None:
assert format_ms_time(date_in_epoch_ms) == res.astimezone(None).strftime('%Y-%m-%dT%H:%M:%S')
def test_safe_value_fallback():
dict1 = {'keya': None, 'keyb': 2, 'keyc': 5, 'keyd': None}
dict2 = {'keya': 20, 'keyb': None, 'keyc': 6, 'keyd': None}
assert safe_value_fallback(dict1, dict2, 'keya', 'keya') == 20
assert safe_value_fallback(dict2, dict1, 'keya', 'keya') == 20
assert safe_value_fallback(dict1, dict2, 'keyb', 'keyb') == 2
assert safe_value_fallback(dict2, dict1, 'keyb', 'keyb') == 2
assert safe_value_fallback(dict1, dict2, 'keyc', 'keyc') == 5
assert safe_value_fallback(dict2, dict1, 'keyc', 'keyc') == 6
assert safe_value_fallback(dict1, dict2, 'keyd', 'keyd') is None
assert safe_value_fallback(dict2, dict1, 'keyd', 'keyd') is None
assert safe_value_fallback(dict2, dict1, 'keyd', 'keyd', 1234) == 1234
assert safe_value_fallback(dict1, dict2, 'keyNo', 'keyNo') is None
assert safe_value_fallback(dict2, dict1, 'keyNo', 'keyNo') is None
assert safe_value_fallback(dict2, dict1, 'keyNo', 'keyNo', 1234) == 1234
def test_plural() -> None:
assert plural(0, "page") == "pages"
assert plural(0.0, "page") == "pages"
@@ -123,3 +146,17 @@ def test_plural() -> None:
assert plural(1.5, "ox", "oxen") == "oxen"
assert plural(-0.5, "ox", "oxen") == "oxen"
assert plural(-1.5, "ox", "oxen") == "oxen"
def test_render_template_fallback(mocker):
from jinja2.exceptions import TemplateNotFound
with pytest.raises(TemplateNotFound):
val = render_template(
templatefile='subtemplates/indicators_does-not-exist.j2',)
val = render_template_with_fallback(
templatefile='subtemplates/indicators_does-not-exist.j2',
templatefallbackfile='subtemplates/indicators_minimal.j2',
)
assert isinstance(val, str)
assert 'if self.dp' in val

View File

@@ -9,53 +9,7 @@ from sqlalchemy import create_engine
from freqtrade import constants
from freqtrade.exceptions import OperationalException
from freqtrade.persistence import Trade, clean_dry_run_db, init
from tests.conftest import log_has
def create_mock_trades(fee):
"""
Create some fake trades ...
"""
# Simulate dry_run entries
trade = Trade(
pair='ETH/BTC',
stake_amount=0.001,
amount=123.0,
fee_open=fee.return_value,
fee_close=fee.return_value,
open_rate=0.123,
exchange='bittrex',
open_order_id='dry_run_buy_12345'
)
Trade.session.add(trade)
trade = Trade(
pair='ETC/BTC',
stake_amount=0.001,
amount=123.0,
fee_open=fee.return_value,
fee_close=fee.return_value,
open_rate=0.123,
close_rate=0.128,
close_profit=0.005,
exchange='bittrex',
is_open=False,
open_order_id='dry_run_sell_12345'
)
Trade.session.add(trade)
# Simulate prod entry
trade = Trade(
pair='ETC/BTC',
stake_amount=0.001,
amount=123.0,
fee_open=fee.return_value,
fee_close=fee.return_value,
open_rate=0.123,
exchange='bittrex',
open_order_id='prod_buy_12345'
)
Trade.session.add(trade)
from tests.conftest import log_has, create_mock_trades
def test_init_create_session(default_conf):
@@ -476,12 +430,22 @@ def test_migrate_old(mocker, default_conf, fee):
stake=default_conf.get("stake_amount"),
amount=amount
)
insert_table_old2 = """INSERT INTO trades (exchange, pair, is_open, fee,
open_rate, close_rate, stake_amount, amount, open_date)
VALUES ('BITTREX', 'BTC_ETC', 0, {fee},
0.00258580, 0.00268580, {stake}, {amount},
'2017-11-28 12:44:24.000000')
""".format(fee=fee.return_value,
stake=default_conf.get("stake_amount"),
amount=amount
)
engine = create_engine('sqlite://')
mocker.patch('freqtrade.persistence.create_engine', lambda *args, **kwargs: engine)
# Create table using the old format
engine.execute(create_table_old)
engine.execute(insert_table_old)
engine.execute(insert_table_old2)
# Run init to test migration
init(default_conf['db_url'], default_conf['dry_run'])
@@ -500,6 +464,20 @@ def test_migrate_old(mocker, default_conf, fee):
assert trade.stop_loss == 0.0
assert trade.initial_stop_loss == 0.0
assert trade.open_trade_price == trade._calc_open_trade_price()
assert trade.close_profit_abs is None
assert trade.fee_open_cost is None
assert trade.fee_open_currency is None
assert trade.fee_close_cost is None
assert trade.fee_close_currency is None
trade = Trade.query.filter(Trade.id == 2).first()
assert trade.close_rate is not None
assert trade.is_open == 0
assert trade.open_rate_requested is None
assert trade.close_rate_requested is None
assert trade.close_rate is not None
assert pytest.approx(trade.close_profit_abs) == trade.calc_profit()
assert trade.sell_order_status is None
def test_migrate_new(mocker, default_conf, fee, caplog):
@@ -583,6 +561,7 @@ def test_migrate_new(mocker, default_conf, fee, caplog):
assert log_has("trying trades_bak2", caplog)
assert log_has("Running database migration - backup available as trades_bak2", caplog)
assert trade.open_trade_price == trade._calc_open_trade_price()
assert trade.close_profit_abs is None
def test_migrate_mid_state(mocker, default_conf, fee, caplog):
@@ -757,18 +736,36 @@ def test_to_json(default_conf, fee):
assert result == {'trade_id': None,
'pair': 'ETH/BTC',
'is_open': None,
'open_date_hum': '2 hours ago',
'open_date': trade.open_date.strftime("%Y-%m-%d %H:%M:%S"),
'open_order_id': 'dry_run_buy_12345',
'close_date_hum': None,
'close_date': None,
'open_rate': 0.123,
'open_rate_requested': None,
'open_trade_price': 15.1668225,
'fee_close': 0.0025,
'fee_close_cost': None,
'fee_close_currency': None,
'fee_open': 0.0025,
'fee_open_cost': None,
'fee_open_currency': None,
'close_rate': None,
'close_rate_requested': None,
'amount': 123.0,
'stake_amount': 0.001,
'close_profit': None,
'sell_reason': None,
'sell_order_status': None,
'stop_loss': None,
'stop_loss_pct': None,
'initial_stop_loss': None,
'initial_stop_loss_pct': None}
'initial_stop_loss_pct': None,
'min_rate': None,
'max_rate': None,
'strategy': None,
'ticker_interval': None}
# Simulate dry_run entries
trade = Trade(
@@ -799,7 +796,25 @@ def test_to_json(default_conf, fee):
'stop_loss': None,
'stop_loss_pct': None,
'initial_stop_loss': None,
'initial_stop_loss_pct': None}
'initial_stop_loss_pct': None,
'close_profit': None,
'close_rate_requested': None,
'fee_close': 0.0025,
'fee_close_cost': None,
'fee_close_currency': None,
'fee_open': 0.0025,
'fee_open_cost': None,
'fee_open_currency': None,
'is_open': None,
'max_rate': None,
'min_rate': None,
'open_order_id': None,
'open_rate_requested': None,
'open_trade_price': 12.33075,
'sell_reason': None,
'sell_order_status': None,
'strategy': None,
'ticker_interval': None}
def test_stoploss_reinitialization(default_conf, fee):
@@ -862,6 +877,75 @@ def test_stoploss_reinitialization(default_conf, fee):
assert trade_adj.initial_stop_loss_pct == -0.04
def test_update_fee(fee):
trade = Trade(
pair='ETH/BTC',
stake_amount=0.001,
fee_open=fee.return_value,
open_date=arrow.utcnow().shift(hours=-2).datetime,
amount=10,
fee_close=fee.return_value,
exchange='bittrex',
open_rate=1,
max_rate=1,
)
fee_cost = 0.15
fee_currency = 'BTC'
fee_rate = 0.0075
assert trade.fee_open_currency is None
assert not trade.fee_updated('buy')
assert not trade.fee_updated('sell')
trade.update_fee(fee_cost, fee_currency, fee_rate, 'buy')
assert trade.fee_updated('buy')
assert not trade.fee_updated('sell')
assert trade.fee_open_currency == fee_currency
assert trade.fee_open_cost == fee_cost
assert trade.fee_open == fee_rate
# Setting buy rate should "guess" close rate
assert trade.fee_close == fee_rate
assert trade.fee_close_currency is None
assert trade.fee_close_cost is None
fee_rate = 0.0076
trade.update_fee(fee_cost, fee_currency, fee_rate, 'sell')
assert trade.fee_updated('buy')
assert trade.fee_updated('sell')
assert trade.fee_close == 0.0076
assert trade.fee_close_cost == fee_cost
assert trade.fee_close == fee_rate
def test_fee_updated(fee):
trade = Trade(
pair='ETH/BTC',
stake_amount=0.001,
fee_open=fee.return_value,
open_date=arrow.utcnow().shift(hours=-2).datetime,
amount=10,
fee_close=fee.return_value,
exchange='bittrex',
open_rate=1,
max_rate=1,
)
assert trade.fee_open_currency is None
assert not trade.fee_updated('buy')
assert not trade.fee_updated('sell')
assert not trade.fee_updated('asdf')
trade.update_fee(0.15, 'BTC', 0.0075, 'buy')
assert trade.fee_updated('buy')
assert not trade.fee_updated('sell')
assert trade.fee_open_currency is not None
assert trade.fee_close_currency is None
trade.update_fee(0.15, 'ABC', 0.0075, 'sell')
assert trade.fee_updated('buy')
assert trade.fee_updated('sell')
assert not trade.fee_updated('asfd')
@pytest.mark.usefixtures("init_persistence")
def test_total_open_trades_stakes(fee):

View File

@@ -49,17 +49,17 @@ def test_init_plotscript(default_conf, mocker, testdatadir):
default_conf['trade_source'] = "file"
default_conf['ticker_interval'] = "5m"
default_conf["datadir"] = testdatadir
default_conf['exportfilename'] = str(testdatadir / "backtest-result_test.json")
default_conf['exportfilename'] = testdatadir / "backtest-result_test.json"
ret = init_plotscript(default_conf)
assert "tickers" in ret
assert "ohlcv" in ret
assert "trades" in ret
assert "pairs" in ret
default_conf['pairs'] = ["TRX/BTC", "ADA/BTC"]
ret = init_plotscript(default_conf)
assert "tickers" in ret
assert "TRX/BTC" in ret["tickers"]
assert "ADA/BTC" in ret["tickers"]
assert "ohlcv" in ret
assert "TRX/BTC" in ret["ohlcv"]
assert "ADA/BTC" in ret["ohlcv"]
def test_add_indicators(default_conf, testdatadir, caplog):
@@ -266,17 +266,17 @@ def test_generate_profit_graph(testdatadir):
filename = testdatadir / "backtest-result_test.json"
trades = load_backtest_data(filename)
timerange = TimeRange.parse_timerange("20180110-20180112")
pairs = ["TRX/BTC", "ADA/BTC"]
pairs = ["TRX/BTC", "XLM/BTC"]
trades = trades[trades['close_time'] < pd.Timestamp('2018-01-12', tz='UTC')]
tickers = history.load_data(datadir=testdatadir,
pairs=pairs,
timeframe='5m',
timerange=timerange
)
data = history.load_data(datadir=testdatadir,
pairs=pairs,
timeframe='5m',
timerange=timerange)
trades = trades[trades['pair'].isin(pairs)]
fig = generate_profit_graph(pairs, tickers, trades, timeframe="5m")
fig = generate_profit_graph(pairs, data, trades, timeframe="5m")
assert isinstance(fig, go.Figure)
assert fig.layout.title.text == "Freqtrade Profit plot"
@@ -292,7 +292,7 @@ def test_generate_profit_graph(testdatadir):
profit = find_trace_in_fig_data(figure.data, "Profit")
assert isinstance(profit, go.Scatter)
profit = find_trace_in_fig_data(figure.data, "Max drawdown 0.00%")
profit = find_trace_in_fig_data(figure.data, "Max drawdown 10.45%")
assert isinstance(profit, go.Scatter)
for pair in pairs:
@@ -318,7 +318,7 @@ def test_start_plot_dataframe(mocker):
def test_load_and_plot_trades(default_conf, mocker, caplog, testdatadir):
default_conf['trade_source'] = 'file'
default_conf["datadir"] = testdatadir
default_conf['exportfilename'] = str(testdatadir / "backtest-result_test.json")
default_conf['exportfilename'] = testdatadir / "backtest-result_test.json"
default_conf['indicators1'] = ["sma5", "ema10"]
default_conf['indicators2'] = ["macd"]
default_conf['pairs'] = ["ETH/BTC", "LTC/BTC"]
@@ -374,7 +374,7 @@ def test_start_plot_profit_error(mocker):
def test_plot_profit(default_conf, mocker, testdatadir, caplog):
default_conf['trade_source'] = 'file'
default_conf["datadir"] = testdatadir
default_conf['exportfilename'] = str(testdatadir / "backtest-result_test.json")
default_conf['exportfilename'] = testdatadir / "backtest-result_test.json"
default_conf['pairs'] = ["ETH/BTC", "LTC/BTC"]
profit_mock = MagicMock()

Binary file not shown.

BIN
tests/testdata/XRP_OLD-trades.json.gz vendored Normal file

Binary file not shown.