Merge branch 'develop' into pr/yazeed/3008

This commit is contained in:
Matthias
2020-08-14 06:58:09 +02:00
160 changed files with 7932 additions and 2670 deletions

View File

@@ -44,7 +44,7 @@ def test_start_new_config(mocker, caplog, exchange):
'stake_currency': 'USDT',
'stake_amount': 100,
'fiat_display_currency': 'EUR',
'ticker_interval': '15m',
'timeframe': '15m',
'dry_run': True,
'exchange_name': exchange,
'exchange_key': 'sampleKey',
@@ -68,7 +68,7 @@ def test_start_new_config(mocker, caplog, exchange):
result = rapidjson.loads(wt_mock.call_args_list[0][0][0],
parse_mode=rapidjson.PM_COMMENTS | rapidjson.PM_TRAILING_COMMAS)
assert result['exchange']['name'] == exchange
assert result['ticker_interval'] == '15m'
assert result['timeframe'] == '15m'
def test_start_new_config_exists(mocker, caplog):

View File

@@ -6,15 +6,17 @@ import pytest
from freqtrade.commands import (start_convert_data, start_create_userdir,
start_download_data, start_hyperopt_list,
start_hyperopt_show, start_list_exchanges,
start_list_hyperopts, start_list_markets,
start_list_strategies, start_list_timeframes,
start_new_hyperopt, start_new_strategy,
start_hyperopt_show, start_list_data,
start_list_exchanges, start_list_hyperopts,
start_list_markets, start_list_strategies,
start_list_timeframes, start_new_hyperopt,
start_new_strategy, 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)
@@ -735,7 +736,7 @@ def test_hyperopt_list(mocker, capsys, hyperopt_results):
args = [
"hyperopt-list",
"--no-details"
"--no-details",
]
pargs = get_args(args)
pargs['config'] = None
@@ -748,7 +749,7 @@ def test_hyperopt_list(mocker, capsys, hyperopt_results):
args = [
"hyperopt-list",
"--best",
"--no-details"
"--no-details",
]
pargs = get_args(args)
pargs['config'] = None
@@ -762,7 +763,7 @@ def test_hyperopt_list(mocker, capsys, hyperopt_results):
args = [
"hyperopt-list",
"--profitable",
"--no-details"
"--no-details",
]
pargs = get_args(args)
pargs['config'] = None
@@ -775,7 +776,7 @@ def test_hyperopt_list(mocker, capsys, hyperopt_results):
" 11/12", " 12/12"])
args = [
"hyperopt-list",
"--profitable"
"--profitable",
]
pargs = get_args(args)
pargs['config'] = None
@@ -791,7 +792,7 @@ def test_hyperopt_list(mocker, capsys, hyperopt_results):
"hyperopt-list",
"--no-details",
"--no-color",
"--min-trades", "20"
"--min-trades", "20",
]
pargs = get_args(args)
pargs['config'] = None
@@ -805,7 +806,7 @@ def test_hyperopt_list(mocker, capsys, hyperopt_results):
"hyperopt-list",
"--profitable",
"--no-details",
"--max-trades", "20"
"--max-trades", "20",
]
pargs = get_args(args)
pargs['config'] = None
@@ -820,7 +821,7 @@ def test_hyperopt_list(mocker, capsys, hyperopt_results):
"hyperopt-list",
"--profitable",
"--no-details",
"--min-avg-profit", "0.11"
"--min-avg-profit", "0.11",
]
pargs = get_args(args)
pargs['config'] = None
@@ -834,7 +835,7 @@ def test_hyperopt_list(mocker, capsys, hyperopt_results):
args = [
"hyperopt-list",
"--no-details",
"--max-avg-profit", "0.10"
"--max-avg-profit", "0.10",
]
pargs = get_args(args)
pargs['config'] = None
@@ -848,7 +849,7 @@ def test_hyperopt_list(mocker, capsys, hyperopt_results):
args = [
"hyperopt-list",
"--no-details",
"--min-total-profit", "0.4"
"--min-total-profit", "0.4",
]
pargs = get_args(args)
pargs['config'] = None
@@ -862,7 +863,35 @@ def test_hyperopt_list(mocker, capsys, hyperopt_results):
args = [
"hyperopt-list",
"--no-details",
"--max-total-profit", "0.4"
"--max-total-profit", "0.4",
]
pargs = get_args(args)
pargs['config'] = None
start_hyperopt_list(pargs)
captured = capsys.readouterr()
assert all(x in captured.out
for x in [" 1/12", " 2/12", " 3/12", " 5/12", " 6/12", " 7/12", " 8/12",
" 9/12", " 11/12"])
assert all(x not in captured.out
for x in [" 4/12", " 10/12", " 12/12"])
args = [
"hyperopt-list",
"--no-details",
"--min-objective", "0.1",
]
pargs = get_args(args)
pargs['config'] = None
start_hyperopt_list(pargs)
captured = capsys.readouterr()
assert all(x in captured.out
for x in [" 10/12"])
assert all(x not in captured.out
for x in [" 1/12", " 2/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12",
" 9/12", " 11/12", " 12/12"])
args = [
"hyperopt-list",
"--no-details",
"--max-objective", "0.1",
]
pargs = get_args(args)
pargs['config'] = None
@@ -877,7 +906,7 @@ def test_hyperopt_list(mocker, capsys, hyperopt_results):
"hyperopt-list",
"--profitable",
"--no-details",
"--min-avg-time", "2000"
"--min-avg-time", "2000",
]
pargs = get_args(args)
pargs['config'] = None
@@ -891,7 +920,7 @@ def test_hyperopt_list(mocker, capsys, hyperopt_results):
args = [
"hyperopt-list",
"--no-details",
"--max-avg-time", "1500"
"--max-avg-time", "1500",
]
pargs = get_args(args)
pargs['config'] = None
@@ -905,14 +934,13 @@ def test_hyperopt_list(mocker, capsys, hyperopt_results):
args = [
"hyperopt-list",
"--no-details",
"--export-csv", "test_file.csv"
"--export-csv", "test_file.csv",
]
pargs = get_args(args)
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 +1069,80 @@ 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
def test_start_list_data(testdatadir, capsys):
args = [
"list-data",
"--data-format-ohlcv",
"json",
"--datadir",
str(testdatadir),
]
pargs = get_args(args)
pargs['config'] = None
start_list_data(pargs)
captured = capsys.readouterr()
assert "Found 16 pair / timeframe combinations." in captured.out
assert "\n| Pair | Timeframe |\n" in captured.out
assert "\n| UNITTEST/BTC | 1m, 5m, 8m, 30m |\n" in captured.out
args = [
"list-data",
"--data-format-ohlcv",
"json",
"--pairs", "XRP/ETH",
"--datadir",
str(testdatadir),
]
pargs = get_args(args)
pargs['config'] = None
start_list_data(pargs)
captured = capsys.readouterr()
assert "Found 2 pair / timeframe combinations." in captured.out
assert "\n| Pair | Timeframe |\n" in captured.out
assert "UNITTEST/BTC" not in captured.out
assert "\n| XRP/ETH | 1m, 5m |\n" in captured.out
@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 4 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

@@ -9,7 +9,7 @@
"fiat_display_currency": "USD", // C++-style comment
"amount_reserve_percent" : 0.05, // And more, tabs before this comment
"dry_run": false,
"ticker_interval": "5m",
"timeframe": "5m",
"trailing_stop": false,
"trailing_stop_positive": 0.005,
"trailing_stop_positive_offset": 0.0051,
@@ -92,7 +92,6 @@
"enabled": false,
"process_throttle_secs": 3600,
"calculate_since_number_of_days": 7,
"capital_available_percentage": 0.5,
"allowed_risk": 0.01,
"stoploss_range_min": -0.01,
"stoploss_range_max": -0.1,

View File

@@ -56,6 +56,7 @@ def patched_configuration_load_config_file(mocker, config) -> None:
def patch_exchange(mocker, api_mock=None, id='bittrex', mock_markets=True) -> None:
mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock(return_value={}))
mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
@@ -92,7 +93,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']))
@@ -162,7 +163,7 @@ def patch_get_signal(freqtrade: FreqtradeBot, value=(True, False)) -> None:
:param value: which value IStrategy.get_signal() must return
:return: None
"""
freqtrade.strategy.get_signal = lambda e, s, t: value
freqtrade.strategy.get_signal = lambda e, s, x: value
freqtrade.exchange.refresh_latest_ohlcv = lambda p: None
@@ -175,6 +176,7 @@ def create_mock_trades(fee):
pair='ETH/BTC',
stake_amount=0.001,
amount=123.0,
amount_requested=123.0,
fee_open=fee.return_value,
fee_close=fee.return_value,
open_rate=0.123,
@@ -187,6 +189,7 @@ def create_mock_trades(fee):
pair='ETC/BTC',
stake_amount=0.001,
amount=123.0,
amount_requested=123.0,
fee_open=fee.return_value,
fee_close=fee.return_value,
open_rate=0.123,
@@ -198,11 +201,26 @@ def create_mock_trades(fee):
)
Trade.session.add(trade)
trade = Trade(
pair='XRP/BTC',
stake_amount=0.001,
amount=123.0,
fee_open=fee.return_value,
fee_close=fee.return_value,
open_rate=0.05,
close_rate=0.06,
close_profit=0.01,
exchange='bittrex',
is_open=False,
)
Trade.session.add(trade)
# Simulate prod entry
trade = Trade(
pair='ETC/BTC',
stake_amount=0.001,
amount=123.0,
amount_requested=124.0,
fee_open=fee.return_value,
fee_close=fee.return_value,
open_rate=0.123,
@@ -247,8 +265,9 @@ def default_conf(testdatadir):
"stake_currency": "BTC",
"stake_amount": 0.001,
"fiat_display_currency": "USD",
"ticker_interval": '5m',
"timeframe": '5m',
"dry_run": True,
"cancel_open_orders_on_exit": False,
"minimal_roi": {
"40": 0.0,
"30": 0.01,
@@ -304,7 +323,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
@@ -658,7 +678,8 @@ def shitcoinmarkets(markets):
Fixture with shitcoin markets - used to test filters in pairlists
"""
shitmarkets = deepcopy(markets)
shitmarkets.update({'HOT/BTC': {
shitmarkets.update({
'HOT/BTC': {
'id': 'HOTBTC',
'symbol': 'HOT/BTC',
'base': 'HOT',
@@ -763,7 +784,32 @@ def shitcoinmarkets(markets):
"spot": True,
"future": False,
"active": True
},
},
'ADADOUBLE/USDT': {
"percentage": True,
"tierBased": False,
"taker": 0.001,
"maker": 0.001,
"precision": {
"base": 8,
"quote": 8,
"amount": 2,
"price": 4
},
"limits": {
},
"id": "ADADOUBLEUSDT",
"symbol": "ADADOUBLE/USDT",
"base": "ADADOUBLE",
"quote": "USDT",
"baseId": "ADADOUBLE",
"quoteId": "USDT",
"info": {},
"type": "spot",
"spot": True,
"future": False,
"active": True
},
})
return shitmarkets
@@ -779,11 +825,12 @@ 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,
'cost': 0.0009999,
'remaining': 0.0,
'status': 'closed'
}
@@ -795,7 +842,7 @@ 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,
@@ -811,7 +858,7 @@ 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,
@@ -827,7 +874,7 @@ 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,
@@ -843,7 +890,7 @@ 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,
@@ -859,7 +906,7 @@ 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,
@@ -873,10 +920,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 {
@@ -1291,6 +1431,28 @@ def tickers():
"quoteVolume": 0.0,
"info": {}
},
"ADADOUBLE/USDT": {
"symbol": "ADADOUBLE/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": 0,
"previousClose": None,
"change": None,
"percentage": 2.628,
"average": None,
"baseVolume": 0.0,
"quoteVolume": 0.0,
"info": {}
},
})
@@ -1328,6 +1490,15 @@ def trades_for_order():
@pytest.fixture(scope="function")
def trades_history():
return [[1565798389463, '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',
@@ -1482,10 +1653,11 @@ 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,
'cost': 1.963528,
'remaining': 90.99181073,
'status': 'closed',
'fee': None
@@ -1601,7 +1773,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
@@ -1612,11 +1784,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,
@@ -1663,8 +1836,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

@@ -43,11 +43,11 @@ def test_load_trades_from_db(default_conf, fee, mocker):
trades = load_trades_from_db(db_url=default_conf['db_url'])
assert init_mock.call_count == 1
assert len(trades) == 3
assert len(trades) == 4
assert isinstance(trades, DataFrame)
assert "pair" in trades.columns
assert "open_time" in trades.columns
assert "profitperc" in trades.columns
assert "profit_percent" in trades.columns
for col in BT_DATA_COLUMNS:
if col not in ['index', 'open_at_end']:
@@ -178,6 +178,10 @@ def test_create_cum_profit1(testdatadir):
assert cum_profits.iloc[0]['cum_profits'] == 0
assert cum_profits.iloc[-1]['cum_profits'] == 0.0798005
with pytest.raises(ValueError, match='Trade dataframe empty.'):
create_cum_profit(df.set_index('date'), bt_data[bt_data["pair"] == 'NOTAPAIR'],
"cum_profits", timeframe="5m")
def test_calculate_max_drawdown(testdatadir):
filename = testdatadir / "backtest-result_test.json"

View File

@@ -5,12 +5,10 @@ from freqtrade.configuration.timerange import TimeRange
from freqtrade.data.converter import (convert_ohlcv_format,
convert_trades_format,
ohlcv_fill_up_missing_data,
ohlcv_to_dataframe,
trim_dataframe)
from freqtrade.data.history import (get_timerange,
load_data,
load_pair_history,
validate_backtest_data)
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
from tests.data.test_history import _backup_file, _clean_test_file
@@ -197,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,15 +1,19 @@
from datetime import datetime, timezone
from unittest.mock import MagicMock
import pytest
from pandas import DataFrame
from freqtrade.data.dataprovider import DataProvider
from freqtrade.exceptions import ExchangeError, OperationalException
from freqtrade.pairlist.pairlistmanager import PairListManager
from freqtrade.state import RunMode
from tests.conftest import get_patched_exchange
def test_ohlcv(mocker, default_conf, ohlcv_history):
default_conf["runmode"] = RunMode.DRY_RUN
timeframe = default_conf["ticker_interval"]
timeframe = default_conf["timeframe"]
exchange = get_patched_exchange(mocker, default_conf)
exchange._klines[("XRP/BTC", timeframe)] = ohlcv_history
exchange._klines[("UNITTEST/BTC", timeframe)] = ohlcv_history
@@ -50,50 +54,47 @@ def test_historic_ohlcv(mocker, default_conf, ohlcv_history):
def test_get_pair_dataframe(mocker, default_conf, ohlcv_history):
default_conf["runmode"] = RunMode.DRY_RUN
ticker_interval = default_conf["ticker_interval"]
timeframe = default_conf["timeframe"]
exchange = get_patched_exchange(mocker, default_conf)
exchange._klines[("XRP/BTC", ticker_interval)] = ohlcv_history
exchange._klines[("UNITTEST/BTC", ticker_interval)] = ohlcv_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 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 ohlcv_history
assert not dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval).empty
assert dp.get_pair_dataframe("NONESENSE/AAA", ticker_interval).empty
assert ohlcv_history.equals(dp.get_pair_dataframe("UNITTEST/BTC", timeframe))
assert isinstance(dp.get_pair_dataframe("UNITTEST/BTC", timeframe), DataFrame)
assert dp.get_pair_dataframe("UNITTEST/BTC", timeframe) is not ohlcv_history
assert not dp.get_pair_dataframe("UNITTEST/BTC", timeframe).empty
assert dp.get_pair_dataframe("NONESENSE/AAA", timeframe).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", timeframe)\
.equals(dp.get_pair_dataframe("UNITTEST/BTC"))
default_conf["runmode"] = RunMode.LIVE
dp = DataProvider(default_conf, exchange)
assert dp.runmode == RunMode.LIVE
assert isinstance(dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval), DataFrame)
assert dp.get_pair_dataframe("NONESENSE/AAA", ticker_interval).empty
assert isinstance(dp.get_pair_dataframe("UNITTEST/BTC", timeframe), DataFrame)
assert dp.get_pair_dataframe("NONESENSE/AAA", timeframe).empty
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)
assert dp.runmode == RunMode.BACKTEST
assert isinstance(dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval), DataFrame)
# assert dp.get_pair_dataframe("NONESENSE/AAA", ticker_interval).empty
assert isinstance(dp.get_pair_dataframe("UNITTEST/BTC", timeframe), DataFrame)
# assert dp.get_pair_dataframe("NONESENSE/AAA", timeframe).empty
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)] = ohlcv_history
exchange._klines[("UNITTEST/BTC", ticker_interval)] = ohlcv_history
timeframe = default_conf["timeframe"]
exchange._klines[("XRP/BTC", timeframe)] = ohlcv_history
exchange._klines[("UNITTEST/BTC", timeframe)] = 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", timeframe), ("UNITTEST/BTC", timeframe), ]
def test_refresh(mocker, default_conf, ohlcv_history):
@@ -101,10 +102,10 @@ def test_refresh(mocker, default_conf, ohlcv_history):
mocker.patch("freqtrade.exchange.Exchange.refresh_latest_ohlcv", refresh_mock)
exchange = get_patched_exchange(mocker, default_conf, id="binance")
ticker_interval = default_conf["ticker_interval"]
pairs = [("XRP/BTC", ticker_interval), ("UNITTEST/BTC", ticker_interval)]
timeframe = default_conf["timeframe"]
pairs = [("XRP/BTC", timeframe), ("UNITTEST/BTC", timeframe)]
pairs_non_trad = [("ETH/USDT", ticker_interval), ("BTC/TUSD", "1h")]
pairs_non_trad = [("ETH/USDT", timeframe), ("BTC/TUSD", "1h")]
dp = DataProvider(default_conf, exchange)
dp.refresh(pairs)
@@ -152,3 +153,71 @@ 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=ExchangeError('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()
def test_get_analyzed_dataframe(mocker, default_conf, ohlcv_history):
default_conf["runmode"] = RunMode.DRY_RUN
timeframe = default_conf["timeframe"]
exchange = get_patched_exchange(mocker, default_conf)
dp = DataProvider(default_conf, exchange)
dp._set_cached_df("XRP/BTC", timeframe, ohlcv_history)
dp._set_cached_df("UNITTEST/BTC", timeframe, ohlcv_history)
assert dp.runmode == RunMode.DRY_RUN
dataframe, time = dp.get_analyzed_dataframe("UNITTEST/BTC", timeframe)
assert ohlcv_history.equals(dataframe)
assert isinstance(time, datetime)
dataframe, time = dp.get_analyzed_dataframe("XRP/BTC", timeframe)
assert ohlcv_history.equals(dataframe)
assert isinstance(time, datetime)
dataframe, time = dp.get_analyzed_dataframe("NOTHING/BTC", timeframe)
assert dataframe.empty
assert isinstance(time, datetime)
assert time == datetime(1970, 1, 1, tzinfo=timezone.utc)

View File

@@ -354,7 +354,7 @@ def test_init(default_conf, mocker) -> None:
assert {} == load_data(
datadir=Path(''),
pairs=[],
timeframe=default_conf['ticker_interval']
timeframe=default_conf['timeframe']
)
@@ -363,13 +363,13 @@ def test_init_with_refresh(default_conf, mocker) -> None:
refresh_data(
datadir=Path(''),
pairs=[],
timeframe=default_conf['ticker_interval'],
timeframe=default_conf['timeframe'],
exchange=exchange
)
assert {} == load_data(
datadir=Path(''),
pairs=[],
timeframe=default_conf['ticker_interval']
timeframe=default_conf['timeframe']
)
@@ -547,6 +547,18 @@ 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
assert ght_mock.call_args_list[0][1]['from_id'] is not None
# clean files freshly downloaded
_clean_test_file(file1)
@@ -557,6 +569,27 @@ def test_download_trades_history(trades_history, mocker, default_conf, testdatad
pair='ETH/BTC')
assert log_has_re('Failed to download historic trades for pair: "ETH/BTC".*', caplog)
file2 = testdatadir / 'XRP_ETH-trades.json.gz'
_backup_file(file2, True)
ght_mock.reset_mock()
mocker.patch('freqtrade.exchange.Exchange.get_historic_trades',
ght_mock)
# Since before first start date
since_time = int(trades_history[0][0] // 1000) - 500
timerange = TimeRange('date', None, since_time, 0)
assert _download_trades_history(data_handler=data_handler, exchange=exchange,
pair='XRP/ETH', timerange=timerange)
assert ght_mock.call_count == 1
assert int(ght_mock.call_args_list[0][1]['since'] // 1000) == since_time
assert ght_mock.call_args_list[0][1]['from_id'] is None
assert log_has_re(r'Start earlier than available data. Redownloading trades for.*', caplog)
_clean_test_file(file2)
def test_convert_trades_to_ohlcv(mocker, default_conf, testdatadir, caplog):
@@ -598,10 +631,24 @@ def test_jsondatahandler_ohlcv_get_pairs(testdatadir):
assert set(pairs) == {'UNITTEST/BTC'}
def test_jsondatahandler_ohlcv_get_available_data(testdatadir):
paircombs = JsonDataHandler.ohlcv_get_available_data(testdatadir)
# Convert to set to avoid failures due to sorting
assert set(paircombs) == {('UNITTEST/BTC', '5m'), ('ETH/BTC', '5m'), ('XLM/BTC', '5m'),
('TRX/BTC', '5m'), ('LTC/BTC', '5m'), ('XMR/BTC', '5m'),
('ZEC/BTC', '5m'), ('UNITTEST/BTC', '1m'), ('ADA/BTC', '5m'),
('ETC/BTC', '5m'), ('NXT/BTC', '5m'), ('DASH/BTC', '5m'),
('XRP/ETH', '1m'), ('XRP/ETH', '5m'), ('UNITTEST/BTC', '30m'),
('UNITTEST/BTC', '8m')}
paircombs = JsonGzDataHandler.ohlcv_get_available_data(testdatadir)
assert set(paircombs) == {('UNITTEST/BTC', '8m')}
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 +661,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

@@ -27,7 +27,7 @@ from tests.optimize import (BTContainer, BTrade, _build_backtest_dataframe,
####################################################################
tests_start_time = arrow.get(2018, 10, 3)
ticker_interval_in_minute = 60
timeframe_in_minute = 60
_ohlc = {'date': 0, 'buy': 1, 'open': 2, 'high': 3, 'low': 4, 'close': 5, 'sell': 6, 'volume': 7}
# Helpers for this test file
@@ -49,7 +49,7 @@ def _build_dataframe(buy_ohlc_sell_matrice):
'date': tests_start_time.shift(
minutes=(
ohlc[0] *
ticker_interval_in_minute)).timestamp *
timeframe_in_minute)).timestamp *
1000,
'buy': ohlc[1],
'open': ohlc[2],
@@ -70,7 +70,7 @@ def _build_dataframe(buy_ohlc_sell_matrice):
def _time_on_candle(number):
return np.datetime64(tests_start_time.shift(
minutes=(number * ticker_interval_in_minute)).timestamp * 1000, 'ms')
minutes=(number * timeframe_in_minute)).timestamp * 1000, 'ms')
# End helper functions
@@ -262,7 +262,7 @@ def mocked_load_data(datadir, pairs=[], timeframe='0m',
NEOBTC = [
[
tests_start_time.shift(minutes=(x * ticker_interval_in_minute)).timestamp * 1000,
tests_start_time.shift(minutes=(x * timeframe_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 = [
[
tests_start_time.shift(minutes=(x * ticker_interval_in_minute)).timestamp * 1000,
tests_start_time.shift(minutes=(x * timeframe_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,
@@ -335,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)
@@ -394,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()
@@ -405,3 +409,98 @@ def test_process_expectancy(mocker, edge_conf):
final = edge._process_expectancy(trades_df)
assert len(final) == 0
assert isinstance(final, dict)
def test_process_expectancy_remove_pumps(mocker, edge_conf, fee,):
edge_conf['edge']['min_trade_number'] = 2
edge_conf['edge']['remove_pumps'] = True
freqtrade = get_patched_freqtradebot(mocker, edge_conf)
freqtrade.exchange.get_fee = fee
edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy)
trades = [
{'pair': 'TEST/BTC',
'stoploss': -0.9,
'profit_percent': '',
'profit_abs': '',
'open_time': np.datetime64('2018-10-03T00:05:00.000000000'),
'close_time': np.datetime64('2018-10-03T00:10:00.000000000'),
'open_index': 1,
'close_index': 1,
'trade_duration': '',
'open_rate': 17,
'close_rate': 15,
'exit_type': 'sell_signal'},
{'pair': 'TEST/BTC',
'stoploss': -0.9,
'profit_percent': '',
'profit_abs': '',
'open_time': np.datetime64('2018-10-03T00:20:00.000000000'),
'close_time': np.datetime64('2018-10-03T00:25:00.000000000'),
'open_index': 4,
'close_index': 4,
'trade_duration': '',
'open_rate': 20,
'close_rate': 10,
'exit_type': 'sell_signal'},
{'pair': 'TEST/BTC',
'stoploss': -0.9,
'profit_percent': '',
'profit_abs': '',
'open_time': np.datetime64('2018-10-03T00:20:00.000000000'),
'close_time': np.datetime64('2018-10-03T00:25:00.000000000'),
'open_index': 4,
'close_index': 4,
'trade_duration': '',
'open_rate': 20,
'close_rate': 10,
'exit_type': 'sell_signal'},
{'pair': 'TEST/BTC',
'stoploss': -0.9,
'profit_percent': '',
'profit_abs': '',
'open_time': np.datetime64('2018-10-03T00:20:00.000000000'),
'close_time': np.datetime64('2018-10-03T00:25:00.000000000'),
'open_index': 4,
'close_index': 4,
'trade_duration': '',
'open_rate': 20,
'close_rate': 10,
'exit_type': 'sell_signal'},
{'pair': 'TEST/BTC',
'stoploss': -0.9,
'profit_percent': '',
'profit_abs': '',
'open_time': np.datetime64('2018-10-03T00:20:00.000000000'),
'close_time': np.datetime64('2018-10-03T00:25:00.000000000'),
'open_index': 4,
'close_index': 4,
'trade_duration': '',
'open_rate': 20,
'close_rate': 10,
'exit_type': 'sell_signal'},
{'pair': 'TEST/BTC',
'stoploss': -0.9,
'profit_percent': '',
'profit_abs': '',
'open_time': np.datetime64('2018-10-03T00:30:00.000000000'),
'close_time': np.datetime64('2018-10-03T00:40:00.000000000'),
'open_index': 6,
'close_index': 7,
'trade_duration': '',
'open_rate': 26,
'close_rate': 134,
'exit_type': 'sell_signal'}
]
trades_df = DataFrame(trades)
trades_df = edge._fill_calculable_fields(trades_df)
final = edge._process_expectancy(trades_df)
assert 'TEST/BTC' in final
assert final['TEST/BTC'].stoploss == -0.9
assert final['TEST/BTC'].nb_trades == len(trades_df) - 1
assert round(final['TEST/BTC'].winrate, 10) == 0.0

View File

@@ -5,11 +5,17 @@ import ccxt
import pytest
from freqtrade.exceptions import (DependencyException, InvalidOrderException,
OperationalException, TemporaryError)
OperationalException)
from tests.conftest import get_patched_exchange
from tests.exchange.test_exchange import ccxt_exceptionhandlers
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 +26,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 +37,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 +47,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
@@ -57,15 +63,9 @@ def test_stoploss_order_binance(default_conf, mocker):
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
with pytest.raises(TemporaryError):
api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError("No connection"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
with pytest.raises(OperationalException, match=r".*DeadBeef.*"):
api_mock.create_order = MagicMock(side_effect=ccxt.BaseError("DeadBeef"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
ccxt_exceptionhandlers(mocker, default_conf, api_mock, "binance",
"stoploss", "create_order", retries=1,
pair='ETH/BTC', amount=1, stop_price=220, order_types={})
def test_stoploss_order_dry_run_binance(default_conf, mocker):

View File

@@ -4,17 +4,17 @@ import copy
import logging
from datetime import datetime, timezone
from random import randint
from unittest.mock import MagicMock, Mock, PropertyMock
from unittest.mock import MagicMock, Mock, PropertyMock, patch
import arrow
import ccxt
import pytest
from pandas import DataFrame
from freqtrade.exceptions import (DependencyException, InvalidOrderException,
from freqtrade.exceptions import (DependencyException, InvalidOrderException, DDosProtection,
OperationalException, TemporaryError)
from freqtrade.exchange import Binance, Exchange, Kraken
from freqtrade.exchange.common import API_RETRY_COUNT
from freqtrade.exchange.common import API_RETRY_COUNT, calculate_backoff
from freqtrade.exchange.exchange import (market_is_active, symbol_is_pair,
timeframe_to_minutes,
timeframe_to_msecs,
@@ -25,7 +25,7 @@ from freqtrade.resolvers.exchange_resolver import ExchangeResolver
from tests.conftest import get_patched_exchange, log_has, log_has_re
# Make sure to always keep one exchange here which is NOT subclassed!!
EXCHANGES = ['bittrex', 'binance', 'kraken', ]
EXCHANGES = ['bittrex', 'binance', 'kraken', 'ftx']
# Source: https://stackoverflow.com/questions/29881236/how-to-mock-asyncio-coroutines
@@ -37,12 +37,20 @@ def get_mock_coro(return_value):
def ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name,
fun, mock_ccxt_fun, **kwargs):
fun, mock_ccxt_fun, retries=API_RETRY_COUNT + 1, **kwargs):
with patch('freqtrade.exchange.common.time.sleep'):
with pytest.raises(DDosProtection):
api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.DDoSProtection("DDos"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
getattr(exchange, fun)(**kwargs)
assert api_mock.__dict__[mock_ccxt_fun].call_count == retries
with pytest.raises(TemporaryError):
api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.NetworkError("DeaDBeef"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
getattr(exchange, fun)(**kwargs)
assert api_mock.__dict__[mock_ccxt_fun].call_count == API_RETRY_COUNT + 1
assert api_mock.__dict__[mock_ccxt_fun].call_count == retries
with pytest.raises(OperationalException):
api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.BaseError("DeadBeef"))
@@ -51,12 +59,21 @@ def ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name,
assert api_mock.__dict__[mock_ccxt_fun].call_count == 1
async def async_ccxt_exception(mocker, default_conf, api_mock, fun, mock_ccxt_fun, **kwargs):
async def async_ccxt_exception(mocker, default_conf, api_mock, fun, mock_ccxt_fun,
retries=API_RETRY_COUNT + 1, **kwargs):
with patch('freqtrade.exchange.common.asyncio.sleep', get_mock_coro(None)):
with pytest.raises(DDosProtection):
api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.DDoSProtection("Dooh"))
exchange = get_patched_exchange(mocker, default_conf, api_mock)
await getattr(exchange, fun)(**kwargs)
assert api_mock.__dict__[mock_ccxt_fun].call_count == retries
with pytest.raises(TemporaryError):
api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.NetworkError("DeadBeef"))
exchange = get_patched_exchange(mocker, default_conf, api_mock)
await getattr(exchange, fun)(**kwargs)
assert api_mock.__dict__[mock_ccxt_fun].call_count == API_RETRY_COUNT + 1
assert api_mock.__dict__[mock_ccxt_fun].call_count == retries
with pytest.raises(OperationalException):
api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.BaseError("DeadBeef"))
@@ -88,15 +105,19 @@ def test_init_ccxt_kwargs(default_conf, mocker, caplog):
caplog.clear()
conf = copy.deepcopy(default_conf)
conf['exchange']['ccxt_config'] = {'TestKWARG': 11}
conf['exchange']['ccxt_sync_config'] = {'TestKWARG44': 11}
conf['exchange']['ccxt_async_config'] = {'asyncio_loop': True}
asynclogmsg = "Applying additional ccxt config: {'TestKWARG': 11, 'asyncio_loop': True}"
ex = Exchange(conf)
assert not log_has("Applying additional ccxt config: {'aiohttp_trust_env': True}", caplog)
assert not ex._api_async.aiohttp_trust_env
assert hasattr(ex._api, 'TestKWARG')
assert ex._api.TestKWARG == 11
assert not hasattr(ex._api_async, 'TestKWARG')
assert log_has("Applying additional ccxt config: {'TestKWARG': 11}", caplog)
# ccxt_config is assigned to both sync and async
assert not hasattr(ex._api_async, 'TestKWARG44')
assert hasattr(ex._api_async, 'TestKWARG')
assert log_has("Applying additional ccxt config: {'TestKWARG': 11, 'TestKWARG44': 11}", caplog)
assert log_has(asynclogmsg, caplog)
def test_destroy(default_conf, mocker, caplog):
@@ -315,7 +336,12 @@ def test_set_sandbox_exception(default_conf, mocker):
def test__load_async_markets(default_conf, mocker, caplog):
exchange = get_patched_exchange(mocker, default_conf)
mocker.patch('freqtrade.exchange.Exchange._init_ccxt')
mocker.patch('freqtrade.exchange.Exchange.validate_pairs')
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes')
mocker.patch('freqtrade.exchange.Exchange._load_markets')
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
exchange = Exchange(default_conf)
exchange._api_async.load_markets = get_mock_coro(None)
exchange._load_async_markets()
assert exchange._api_async.load_markets.call_count == 1
@@ -348,7 +374,7 @@ def test__load_markets(default_conf, mocker, caplog):
assert ex.markets == expected_return
def test__reload_markets(default_conf, mocker, caplog):
def test_reload_markets(default_conf, mocker, caplog):
caplog.set_level(logging.DEBUG)
initial_markets = {'ETH/BTC': {}}
@@ -361,23 +387,26 @@ def test__reload_markets(default_conf, mocker, caplog):
default_conf['exchange']['markets_refresh_interval'] = 10
exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance",
mock_markets=False)
exchange._load_async_markets = MagicMock()
exchange._last_markets_refresh = arrow.utcnow().timestamp
updated_markets = {'ETH/BTC': {}, "LTC/BTC": {}}
assert exchange.markets == initial_markets
# less than 10 minutes have passed, no reload
exchange._reload_markets()
exchange.reload_markets()
assert exchange.markets == initial_markets
assert exchange._load_async_markets.call_count == 0
# more than 10 minutes have passed, reload is executed
exchange._last_markets_refresh = arrow.utcnow().timestamp - 15 * 60
exchange._reload_markets()
exchange.reload_markets()
assert exchange.markets == updated_markets
assert exchange._load_async_markets.call_count == 1
assert log_has('Performing scheduled market reload..', caplog)
def test__reload_markets_exception(default_conf, mocker, caplog):
def test_reload_markets_exception(default_conf, mocker, caplog):
caplog.set_level(logging.DEBUG)
api_mock = MagicMock()
@@ -386,7 +415,7 @@ def test__reload_markets_exception(default_conf, mocker, caplog):
exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance")
# less than 10 minutes have passed, no reload
exchange._reload_markets()
exchange.reload_markets()
assert exchange._last_markets_refresh == 0
assert log_has_re(r"Could not reload markets.*", caplog)
@@ -517,9 +546,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):
@@ -574,7 +603,7 @@ def test_validate_pairs_stakecompatibility_fail(default_conf, mocker, caplog):
('5m'), ("1m"), ("15m"), ("1h")
])
def test_validate_timeframes(default_conf, mocker, timeframe):
default_conf["ticker_interval"] = timeframe
default_conf["timeframe"] = timeframe
api_mock = MagicMock()
id_mock = PropertyMock(return_value='test_exchange')
type(api_mock).id = id_mock
@@ -592,7 +621,7 @@ def test_validate_timeframes(default_conf, mocker, timeframe):
def test_validate_timeframes_failed(default_conf, mocker):
default_conf["ticker_interval"] = "3m"
default_conf["timeframe"] = "3m"
api_mock = MagicMock()
id_mock = PropertyMock(return_value='test_exchange')
type(api_mock).id = id_mock
@@ -609,7 +638,7 @@ def test_validate_timeframes_failed(default_conf, mocker):
with pytest.raises(OperationalException,
match=r"Invalid timeframe '3m'. This exchange supports.*"):
Exchange(default_conf)
default_conf["ticker_interval"] = "15s"
default_conf["timeframe"] = "15s"
with pytest.raises(OperationalException,
match=r"Timeframes < 1m are currently not supported by Freqtrade."):
@@ -617,7 +646,7 @@ def test_validate_timeframes_failed(default_conf, mocker):
def test_validate_timeframes_emulated_ohlcv_1(default_conf, mocker):
default_conf["ticker_interval"] = "3m"
default_conf["timeframe"] = "3m"
api_mock = MagicMock()
id_mock = PropertyMock(return_value='test_exchange')
type(api_mock).id = id_mock
@@ -637,7 +666,7 @@ def test_validate_timeframes_emulated_ohlcv_1(default_conf, mocker):
def test_validate_timeframes_emulated_ohlcvi_2(default_conf, mocker):
default_conf["ticker_interval"] = "3m"
default_conf["timeframe"] = "3m"
api_mock = MagicMock()
id_mock = PropertyMock(return_value='test_exchange')
type(api_mock).id = id_mock
@@ -658,7 +687,7 @@ def test_validate_timeframes_emulated_ohlcvi_2(default_conf, mocker):
def test_validate_timeframes_not_in_config(default_conf, mocker):
del default_conf["ticker_interval"]
del default_conf["timeframe"]
api_mock = MagicMock()
id_mock = PropertyMock(return_value='test_exchange')
type(api_mock).id = id_mock
@@ -685,13 +714,13 @@ def test_validate_order_types(default_conf, mocker):
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes')
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
mocker.patch('freqtrade.exchange.Exchange.name', 'Bittrex')
default_conf['order_types'] = {
'buy': 'limit',
'sell': 'limit',
'stoploss': 'market',
'stoploss_on_exchange': False
}
Exchange(default_conf)
type(api_mock).has = PropertyMock(return_value={'createMarketOrder': False})
@@ -701,9 +730,8 @@ def test_validate_order_types(default_conf, mocker):
'buy': 'limit',
'sell': 'limit',
'stoploss': 'market',
'stoploss_on_exchange': 'false'
'stoploss_on_exchange': False
}
with pytest.raises(OperationalException,
match=r'Exchange .* does not support market orders.'):
Exchange(default_conf)
@@ -714,7 +742,6 @@ def test_validate_order_types(default_conf, mocker):
'stoploss': 'limit',
'stoploss_on_exchange': True
}
with pytest.raises(OperationalException,
match=r'On exchange stoploss is not supported for .*'):
Exchange(default_conf)
@@ -1115,9 +1142,10 @@ def test_get_balance_prod(default_conf, mocker, exchange_name):
exchange.get_balance(currency='BTC')
def test_get_balances_dry_run(default_conf, mocker):
@pytest.mark.parametrize("exchange_name", EXCHANGES)
def test_get_balances_dry_run(default_conf, mocker, exchange_name):
default_conf['dry_run'] = True
exchange = get_patched_exchange(mocker, default_conf)
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
assert exchange.get_balances() == {}
@@ -1254,7 +1282,8 @@ def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name):
exchange._async_get_candle_history = Mock(wraps=mock_candle_hist)
# one_call calculation * 1.8 should do 2 calls
since = 5 * 60 * 500 * 1.8
since = 5 * 60 * exchange._ft_has['ohlcv_candle_limit'] * 1.8
ret = exchange.get_historic_ohlcv(pair, "5m", int((arrow.utcnow().timestamp - since) * 1000))
assert exchange._async_get_candle_history.call_count == 2
@@ -1346,7 +1375,7 @@ async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_
# exchange = Exchange(default_conf)
await async_ccxt_exception(mocker, default_conf, MagicMock(),
"_async_get_candle_history", "fetch_ohlcv",
pair='ABCD/BTC', timeframe=default_conf['ticker_interval'])
pair='ABCD/BTC', timeframe=default_conf['timeframe'])
api_mock = MagicMock()
with pytest.raises(OperationalException,
@@ -1413,13 +1442,13 @@ def test_refresh_latest_ohlcv_inv_result(default_conf, mocker, caplog):
@pytest.mark.parametrize("exchange_name", EXCHANGES)
def test_get_order_book(default_conf, mocker, order_book_l2, exchange_name):
def test_fetch_l2_order_book(default_conf, mocker, order_book_l2, exchange_name):
default_conf['exchange']['name'] = exchange_name
api_mock = MagicMock()
api_mock.fetch_l2_order_book = order_book_l2
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
order_book = exchange.get_order_book(pair='ETH/BTC', limit=10)
order_book = exchange.fetch_l2_order_book(pair='ETH/BTC', limit=10)
assert 'bids' in order_book
assert 'asks' in order_book
assert len(order_book['bids']) == 10
@@ -1427,20 +1456,20 @@ def test_get_order_book(default_conf, mocker, order_book_l2, exchange_name):
@pytest.mark.parametrize("exchange_name", EXCHANGES)
def test_get_order_book_exception(default_conf, mocker, exchange_name):
def test_fetch_l2_order_book_exception(default_conf, mocker, exchange_name):
api_mock = MagicMock()
with pytest.raises(OperationalException):
api_mock.fetch_l2_order_book = MagicMock(side_effect=ccxt.NotSupported("Not supported"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
exchange.get_order_book(pair='ETH/BTC', limit=50)
exchange.fetch_l2_order_book(pair='ETH/BTC', limit=50)
with pytest.raises(TemporaryError):
api_mock.fetch_l2_order_book = MagicMock(side_effect=ccxt.NetworkError("DeadBeef"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
exchange.get_order_book(pair='ETH/BTC', limit=50)
exchange.fetch_l2_order_book(pair='ETH/BTC', limit=50)
with pytest.raises(OperationalException):
api_mock.fetch_l2_order_book = MagicMock(side_effect=ccxt.BaseError("DeadBeef"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
exchange.get_order_book(pair='ETH/BTC', limit=50)
exchange.fetch_l2_order_book(pair='ETH/BTC', limit=50)
def make_fetch_ohlcv_mock(data):
@@ -1476,7 +1505,7 @@ async def test___async_get_candle_history_sort(default_conf, mocker, exchange_na
exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv)
sort_mock = mocker.patch('freqtrade.exchange.exchange.sorted', MagicMock(side_effect=sort_data))
# Test the OHLCV data sort
res = await exchange._async_get_candle_history('ETH/BTC', default_conf['ticker_interval'])
res = await exchange._async_get_candle_history('ETH/BTC', default_conf['timeframe'])
assert res[0] == 'ETH/BTC'
res_ohlcv = res[2]
@@ -1513,9 +1542,9 @@ async def test___async_get_candle_history_sort(default_conf, mocker, exchange_na
# Reset sort mock
sort_mock = mocker.patch('freqtrade.exchange.sorted', MagicMock(side_effect=sort_data))
# Test the OHLCV data sort
res = await exchange._async_get_candle_history('ETH/BTC', default_conf['ticker_interval'])
res = await exchange._async_get_candle_history('ETH/BTC', default_conf['timeframe'])
assert res[0] == 'ETH/BTC'
assert res[1] == default_conf['ticker_interval']
assert res[1] == default_conf['timeframe']
res_ohlcv = res[2]
# Sorted not called again - data is already in order
assert sort_mock.call_count == 0
@@ -1537,18 +1566,18 @@ async def test___async_get_candle_history_sort(default_conf, mocker, exchange_na
@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
@@ -1594,7 +1623,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:
@@ -1604,8 +1633,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
@@ -1614,7 +1643,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
@@ -1630,7 +1659,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:]
@@ -1640,8 +1669,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
@@ -1650,11 +1679,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)
@@ -1666,7 +1695,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 []
@@ -1676,8 +1705,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
@@ -1686,7 +1715,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)
@@ -1698,8 +1727,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,
@@ -1720,8 +1749,8 @@ 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)
@@ -1729,6 +1758,7 @@ 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') == {}
assert exchange.cancel_stoploss_order(order_id='123', pair='TKN/BTC') == {}
@pytest.mark.parametrize("exchange_name", EXCHANGES)
@@ -1788,7 +1818,7 @@ def test_cancel_order_with_result_error(default_conf, mocker, exchange_name, cap
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 cancel order 1234 for ETH/BTC.", caplog)
assert log_has("Could not fetch cancelled order 1234.", caplog)
assert res['amount'] == 1541
@@ -1814,31 +1844,95 @@ def test_cancel_order(default_conf, mocker, exchange_name):
@pytest.mark.parametrize("exchange_name", EXCHANGES)
def test_get_order(default_conf, mocker, exchange_name):
def test_cancel_stoploss_order(default_conf, mocker, exchange_name):
default_conf['dry_run'] = False
api_mock = MagicMock()
api_mock.cancel_order = MagicMock(return_value=123)
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
assert exchange.cancel_stoploss_order(order_id='_', pair='TKN/BTC') == 123
with pytest.raises(InvalidOrderException):
api_mock.cancel_order = MagicMock(side_effect=ccxt.InvalidOrder("Did not find order"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
exchange.cancel_stoploss_order(order_id='_', pair='TKN/BTC')
assert api_mock.cancel_order.call_count == 1
ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name,
"cancel_stoploss_order", "cancel_order",
order_id='_', pair='TKN/BTC')
@pytest.mark.parametrize("exchange_name", EXCHANGES)
def test_fetch_order(default_conf, mocker, exchange_name):
default_conf['dry_run'] = True
order = MagicMock()
order.myid = 123
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
exchange._dry_run_open_orders['X'] = order
assert exchange.get_order('X', 'TKN/BTC').myid == 123
assert exchange.fetch_order('X', 'TKN/BTC').myid == 123
with pytest.raises(InvalidOrderException, match=r'Tried to get an invalid dry-run-order.*'):
exchange.get_order('Y', 'TKN/BTC')
exchange.fetch_order('Y', 'TKN/BTC')
default_conf['dry_run'] = False
api_mock = MagicMock()
api_mock.fetch_order = MagicMock(return_value=456)
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
assert exchange.get_order('X', 'TKN/BTC') == 456
assert exchange.fetch_order('X', 'TKN/BTC') == 456
with pytest.raises(InvalidOrderException):
api_mock.fetch_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
exchange.get_order(order_id='_', pair='TKN/BTC')
exchange.fetch_order(order_id='_', pair='TKN/BTC')
assert api_mock.fetch_order.call_count == 1
api_mock.fetch_order = MagicMock(side_effect=ccxt.OrderNotFound("Order not found"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
with patch('freqtrade.exchange.common.time.sleep') as tm:
with pytest.raises(InvalidOrderException):
exchange.fetch_order(order_id='_', pair='TKN/BTC')
# Ensure backoff is called
assert tm.call_args_list[0][0][0] == 1
assert tm.call_args_list[1][0][0] == 2
assert tm.call_args_list[2][0][0] == 5
assert tm.call_args_list[3][0][0] == 10
assert api_mock.fetch_order.call_count == 6
ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name,
'fetch_order', 'fetch_order', retries=6,
order_id='_', pair='TKN/BTC')
@pytest.mark.parametrize("exchange_name", EXCHANGES)
def test_fetch_stoploss_order(default_conf, mocker, exchange_name):
# Don't test FTX here - that needs a seperate test
if exchange_name == 'ftx':
return
default_conf['dry_run'] = True
order = MagicMock()
order.myid = 123
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
exchange._dry_run_open_orders['X'] = order
assert exchange.fetch_stoploss_order('X', 'TKN/BTC').myid == 123
with pytest.raises(InvalidOrderException, match=r'Tried to get an invalid dry-run-order.*'):
exchange.fetch_stoploss_order('Y', 'TKN/BTC')
default_conf['dry_run'] = False
api_mock = MagicMock()
api_mock.fetch_order = MagicMock(return_value=456)
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
assert exchange.fetch_stoploss_order('X', 'TKN/BTC') == 456
with pytest.raises(InvalidOrderException):
api_mock.fetch_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
exchange.fetch_stoploss_order(order_id='_', pair='TKN/BTC')
assert api_mock.fetch_order.call_count == 1
ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name,
'get_order', 'fetch_order',
'fetch_stoploss_order', 'fetch_order',
retries=6,
order_id='_', pair='TKN/BTC')
@@ -2046,6 +2140,13 @@ def test_get_markets(default_conf, mocker, markets,
assert sorted(pairs.keys()) == sorted(expected_keys)
def test_get_markets_error(default_conf, mocker):
ex = get_patched_exchange(mocker, default_conf)
mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=None))
with pytest.raises(OperationalException, match="Markets were not loaded."):
ex.get_markets('LTC', 'USDT', True, False)
def test_timeframe_to_minutes():
assert timeframe_to_minutes("5m") == 5
assert timeframe_to_minutes("10m") == 10
@@ -2145,3 +2246,88 @@ 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),
# 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),
# 0.1% filled - no costs (kraken - #3431)
({'symbol': 'ETH/BTC', 'amount': 0.04, 'cost': 0.0,
'fee': {'currency': 'BTC', 'cost': 0.0, 'rate': None}}, None),
({'symbol': 'ETH/BTC', 'amount': 0.04, 'cost': 0.0,
'fee': {'currency': 'ETH', 'cost': 0.0, 'rate': None}}, 0.0),
({'symbol': 'ETH/BTC', 'amount': 0.04, 'cost': 0.0,
'fee': {'currency': 'NEO', 'cost': 0.0, 'rate': None}}, None),
])
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
@pytest.mark.parametrize('retrycount,max_retries,expected', [
(0, 3, 10),
(1, 3, 5),
(2, 3, 2),
(3, 3, 1),
(0, 1, 2),
(1, 1, 1),
(0, 4, 17),
(1, 4, 10),
(2, 4, 5),
(3, 4, 2),
(4, 4, 1),
(0, 5, 26),
(1, 5, 17),
(2, 5, 10),
(3, 5, 5),
(4, 5, 2),
(5, 5, 1),
])
def test_calculate_backoff(retrycount, max_retries, expected):
assert calculate_backoff(retrycount, max_retries) == expected

158
tests/exchange/test_ftx.py Normal file
View File

@@ -0,0 +1,158 @@
# pragma pylint: disable=missing-docstring, C0103, bad-continuation, global-statement
# pragma pylint: disable=protected-access
from random import randint
from unittest.mock import MagicMock
import ccxt
import pytest
from freqtrade.exceptions import DependencyException, InvalidOrderException
from tests.conftest import get_patched_exchange
from .test_exchange import ccxt_exceptionhandlers
STOPLOSS_ORDERTYPE = 'stop'
def test_stoploss_order_ftx(default_conf, mocker):
api_mock = MagicMock()
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
api_mock.create_order = MagicMock(return_value={
'id': order_id,
'info': {
'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)
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx')
# stoploss_on_exchange_limit_ratio is irrelevant for ftx market orders
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190,
order_types={'stoploss_on_exchange_limit_ratio': 1.05})
assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC'
assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE
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'] == 190
assert 'orderPrice' not in api_mock.create_order.call_args_list[0][1]['params']
assert api_mock.create_order.call_count == 1
api_mock.create_order.reset_mock()
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
assert 'id' in order
assert 'info' in order
assert order['id'] == order_id
assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC'
assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE
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
assert 'orderPrice' not in api_mock.create_order.call_args_list[0][1]['params']
api_mock.create_order.reset_mock()
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220,
order_types={'stoploss': 'limit'})
assert 'id' in order
assert 'info' in order
assert order['id'] == order_id
assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC'
assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE
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
assert 'orderPrice' in api_mock.create_order.call_args_list[0][1]['params']
assert api_mock.create_order.call_args_list[0][1]['params']['orderPrice'] == 217.8
# test exception handling
with pytest.raises(DependencyException):
api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx')
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
with pytest.raises(InvalidOrderException):
api_mock.create_order = MagicMock(
side_effect=ccxt.InvalidOrder("ftx Order would trigger immediately."))
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx')
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
ccxt_exceptionhandlers(mocker, default_conf, api_mock, "ftx",
"stoploss", "create_order", retries=1,
pair='ETH/BTC', amount=1, stop_price=220, order_types={})
def test_stoploss_order_dry_run_ftx(default_conf, mocker):
api_mock = MagicMock()
default_conf['dry_run'] = True
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)
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx')
api_mock.create_order.reset_mock()
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
assert 'id' in order
assert 'info' in order
assert 'type' in order
assert order['type'] == STOPLOSS_ORDERTYPE
assert order['price'] == 220
assert order['amount'] == 1
def test_stoploss_adjust_ftx(mocker, default_conf):
exchange = get_patched_exchange(mocker, default_conf, id='ftx')
order = {
'type': STOPLOSS_ORDERTYPE,
'price': 1500,
}
assert exchange.stoploss_adjust(1501, order)
assert not exchange.stoploss_adjust(1499, order)
# Test with invalid order case ...
order['type'] = 'stop_loss_limit'
assert not exchange.stoploss_adjust(1501, order)
def test_fetch_stoploss_order(default_conf, mocker):
default_conf['dry_run'] = True
order = MagicMock()
order.myid = 123
exchange = get_patched_exchange(mocker, default_conf, id='ftx')
exchange._dry_run_open_orders['X'] = order
assert exchange.fetch_stoploss_order('X', 'TKN/BTC').myid == 123
with pytest.raises(InvalidOrderException, match=r'Tried to get an invalid dry-run-order.*'):
exchange.fetch_stoploss_order('Y', 'TKN/BTC')
default_conf['dry_run'] = False
api_mock = MagicMock()
api_mock.fetch_orders = MagicMock(return_value=[{'id': 'X', 'status': '456'}])
exchange = get_patched_exchange(mocker, default_conf, api_mock, id='ftx')
assert exchange.fetch_stoploss_order('X', 'TKN/BTC')['status'] == '456'
api_mock.fetch_orders = MagicMock(return_value=[{'id': 'Y', 'status': '456'}])
exchange = get_patched_exchange(mocker, default_conf, api_mock, id='ftx')
with pytest.raises(InvalidOrderException, match=r"Could not get stoploss order for id X"):
exchange.fetch_stoploss_order('X', 'TKN/BTC')['status']
with pytest.raises(InvalidOrderException):
api_mock.fetch_orders = MagicMock(side_effect=ccxt.InvalidOrder("Order not found"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, id='ftx')
exchange.fetch_stoploss_order(order_id='_', pair='TKN/BTC')
assert api_mock.fetch_orders.call_count == 1
ccxt_exceptionhandlers(mocker, default_conf, api_mock, 'ftx',
'fetch_stoploss_order', 'fetch_orders',
retries=6,
order_id='_', pair='TKN/BTC')

View File

@@ -6,11 +6,12 @@ from unittest.mock import MagicMock
import ccxt
import pytest
from freqtrade.exceptions import (DependencyException, InvalidOrderException,
OperationalException, TemporaryError)
from freqtrade.exceptions import DependencyException, InvalidOrderException
from tests.conftest import get_patched_exchange
from tests.exchange.test_exchange import ccxt_exceptionhandlers
STOPLOSS_ORDERTYPE = 'stop-loss'
def test_buy_kraken_trading_agreement(default_conf, mocker):
api_mock = MagicMock()
@@ -159,7 +160,6 @@ def test_get_balances_prod(default_conf, mocker):
def test_stoploss_order_kraken(default_conf, mocker):
api_mock = MagicMock()
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
order_type = 'stop-loss'
api_mock.create_order = MagicMock(return_value={
'id': order_id,
@@ -187,7 +187,7 @@ def test_stoploss_order_kraken(default_conf, mocker):
assert 'info' in order
assert order['id'] == order_id
assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC'
assert api_mock.create_order.call_args_list[0][1]['type'] == order_type
assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE
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
@@ -205,20 +205,13 @@ def test_stoploss_order_kraken(default_conf, mocker):
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken')
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
with pytest.raises(TemporaryError):
api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError("No connection"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken')
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
with pytest.raises(OperationalException, match=r".*DeadBeef.*"):
api_mock.create_order = MagicMock(side_effect=ccxt.BaseError("DeadBeef"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken')
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
ccxt_exceptionhandlers(mocker, default_conf, api_mock, "kraken",
"stoploss", "create_order", retries=1,
pair='ETH/BTC', amount=1, stop_price=220, order_types={})
def test_stoploss_order_dry_run_kraken(default_conf, mocker):
api_mock = MagicMock()
order_type = 'stop-loss'
default_conf['dry_run'] = True
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)
@@ -233,7 +226,7 @@ def test_stoploss_order_dry_run_kraken(default_conf, mocker):
assert 'info' in order
assert 'type' in order
assert order['type'] == order_type
assert order['type'] == STOPLOSS_ORDERTYPE
assert order['price'] == 220
assert order['amount'] == 1
@@ -241,7 +234,7 @@ def test_stoploss_order_dry_run_kraken(default_conf, mocker):
def test_stoploss_adjust_kraken(mocker, default_conf):
exchange = get_patched_exchange(mocker, default_conf, id='kraken')
order = {
'type': 'stop-loss',
'type': STOPLOSS_ORDERTYPE,
'price': 1500,
}
assert exchange.stoploss_adjust(1501, order)

View File

@@ -360,7 +360,7 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None:
"""
default_conf["stoploss"] = data.stop_loss
default_conf["minimal_roi"] = data.roi
default_conf["ticker_interval"] = tests_timeframe
default_conf["timeframe"] = tests_timeframe
default_conf["trailing_stop"] = data.trailing_stop
default_conf["trailing_only_offset_is_reached"] = data.trailing_only_offset_is_reached
# Only add this to configuration If it's necessary

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
@@ -80,7 +81,7 @@ def load_data_test(what, testdatadir):
def simple_backtest(config, contour, num_results, mocker, testdatadir) -> None:
patch_exchange(mocker)
config['ticker_interval'] = '1m'
config['timeframe'] = '1m'
backtesting = Backtesting(config)
data = load_data_test(contour, testdatadir)
@@ -164,7 +165,7 @@ def test_setup_optimize_configuration_without_arguments(mocker, default_conf, ca
assert 'pair_whitelist' in config['exchange']
assert 'datadir' in config
assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog)
assert 'ticker_interval' in config
assert 'timeframe' in config
assert not log_has_re('Parameter -i/--ticker-interval detected .*', caplog)
assert 'position_stacking' not in config
@@ -188,7 +189,7 @@ def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) ->
'--config', 'config.json',
'--strategy', 'DefaultStrategy',
'--datadir', '/foo/bar',
'--ticker-interval', '1m',
'--timeframe', '1m',
'--enable-position-stacking',
'--disable-max-market-positions',
'--timerange', ':100',
@@ -207,8 +208,8 @@ def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) ->
assert config['runmode'] == RunMode.BACKTEST
assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog)
assert 'ticker_interval' in config
assert log_has('Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...',
assert 'timeframe' in config
assert log_has('Parameter -i/--timeframe detected ... Using timeframe: 1m ...',
caplog)
assert 'position_stacking' in config
@@ -285,9 +286,9 @@ def test_backtesting_init(mocker, default_conf, order_types) -> None:
assert not backtesting.strategy.order_types["stoploss_on_exchange"]
def test_backtesting_init_no_ticker_interval(mocker, default_conf, caplog) -> None:
def test_backtesting_init_no_timeframe(mocker, default_conf, caplog) -> None:
patch_exchange(mocker)
del default_conf['ticker_interval']
del default_conf['timeframe']
default_conf['strategy_list'] = ['DefaultStrategy',
'SampleStrategy']
@@ -307,6 +308,11 @@ def test_data_with_fee(default_conf, mocker, testdatadir) -> None:
assert backtesting.fee == 0.1234
assert fee_mock.call_count == 0
default_conf['fee'] = 0.0
backtesting = Backtesting(default_conf)
assert backtesting.fee == 0.0
assert fee_mock.call_count == 0
def test_data_to_dataframe_bt(default_conf, mocker, testdatadir) -> None:
patch_exchange(mocker)
@@ -332,10 +338,12 @@ def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None:
mocker.patch('freqtrade.data.history.get_timerange', get_timerange)
patch_exchange(mocker)
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest')
mocker.patch('freqtrade.optimize.backtesting.generate_backtest_stats')
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['timeframe'] = '1m'
default_conf['datadir'] = testdatadir
default_conf['export'] = None
default_conf['timerange'] = '-1510694220'
@@ -362,10 +370,10 @@ def test_backtesting_start_no_data(default_conf, mocker, caplog, testdatadir) ->
mocker.patch('freqtrade.data.history.get_timerange', get_timerange)
patch_exchange(mocker)
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['timeframe'] = "1m"
default_conf['datadir'] = testdatadir
default_conf['export'] = None
default_conf['timerange'] = '20180101-20180102'
@@ -375,6 +383,61 @@ 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['timeframe'] = "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_backtesting_pairlist_list(default_conf, mocker, caplog, testdatadir, tickers) -> None:
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
mocker.patch('freqtrade.exchange.Exchange.get_tickers', tickers)
mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y)
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=['XRP/BTC']))
mocker.patch('freqtrade.pairlist.pairlistmanager.PairListManager.refresh_pairlist')
default_conf['ticker_interval'] = "1m"
default_conf['datadir'] = testdatadir
default_conf['export'] = None
# Use stoploss from strategy
del default_conf['stoploss']
default_conf['timerange'] = '20180101-20180102'
default_conf['pairlists'] = [{"method": "VolumePairList", "number_assets": 5}]
with pytest.raises(OperationalException, match='VolumePairList not allowed for backtesting.'):
Backtesting(default_conf)
default_conf['pairlists'] = [{"method": "StaticPairList"}, {"method": "PrecisionFilter"}, ]
Backtesting(default_conf)
# Multiple strategies
default_conf['strategy_list'] = ['DefaultStrategy', 'TestStrategyLegacy']
with pytest.raises(OperationalException,
match='PrecisionFilter not allowed for backtesting multiple strategies.'):
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)
@@ -428,7 +491,7 @@ def test_backtest(default_conf, fee, mocker, testdatadir) -> None:
t["close_rate"], 6) < round(ln.iloc[0]["high"], 6))
def test_backtest_1min_ticker_interval(default_conf, fee, mocker, testdatadir) -> None:
def test_backtest_1min_timeframe(default_conf, fee, mocker, testdatadir) -> None:
default_conf['ask_strategy']['use_sell_signal'] = False
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
patch_exchange(mocker)
@@ -509,7 +572,7 @@ def test_backtest_alternate_buy_sell(default_conf, fee, mocker, testdatadir):
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
backtest_conf = _make_backtest_conf(mocker, conf=default_conf,
pair='UNITTEST/BTC', datadir=testdatadir)
default_conf['ticker_interval'] = '1m'
default_conf['timeframe'] = '1m'
backtesting = Backtesting(default_conf)
backtesting.strategy.advise_buy = _trend_alternate # Override
backtesting.strategy.advise_sell = _trend_alternate # Override
@@ -530,7 +593,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
@@ -548,7 +611,7 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir)
# Remove data for one pair from the beginning of the data
data[pair] = data[pair][tres:].reset_index()
default_conf['ticker_interval'] = '5m'
default_conf['timeframe'] = '5m'
backtesting = Backtesting(default_conf)
backtesting.strategy.advise_buy = _trend_alternate_hold # Override
@@ -585,12 +648,13 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir)
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.show_backtest_results', MagicMock())
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest')
mocker.patch('freqtrade.optimize.backtesting.generate_backtest_stats')
mocker.patch('freqtrade.optimize.backtesting.show_backtest_results')
mocker.patch('freqtrade.pairlist.pairlistmanager.PairListManager.whitelist',
PropertyMock(return_value=['UNITTEST/BTC']))
patched_configuration_load_config_file(mocker, default_conf)
args = [
@@ -598,7 +662,7 @@ def test_backtest_start_timerange(default_conf, mocker, caplog, testdatadir):
'--config', 'config.json',
'--strategy', 'DefaultStrategy',
'--datadir', str(testdatadir),
'--ticker-interval', '1m',
'--timeframe', '1m',
'--timerange', '1510694220-1510700340',
'--enable-position-stacking',
'--disable-max-market-positions'
@@ -607,7 +671,7 @@ def test_backtest_start_timerange(default_conf, mocker, caplog, testdatadir):
start_backtesting(args)
# check the logs, that will contain the backtest result
exists = [
'Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...',
'Parameter -i/--timeframe detected ... Using timeframe: 1m ...',
'Ignoring max_open_trades (--disable-max-market-positions was used) ...',
'Parameter --timerange detected: 1510694220-1510700340 ...',
f'Using data directory: {testdatadir} ...',
@@ -624,17 +688,26 @@ 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.optimize_reports.generate_text_table', gen_table_mock)
gen_strattable_mock = MagicMock()
mocker.patch('freqtrade.optimize.optimize_reports.generate_text_table_strategy',
gen_strattable_mock)
text_table_mock = MagicMock()
sell_reason_mock = MagicMock()
strattable_mock = MagicMock()
strat_summary = MagicMock()
mocker.patch.multiple('freqtrade.optimize.optimize_reports',
text_table_bt_results=text_table_mock,
text_table_strategy=strattable_mock,
generate_pair_metrics=MagicMock(),
generate_sell_reason_stats=sell_reason_mock,
generate_strategy_metrics=strat_summary,
)
patched_configuration_load_config_file(mocker, default_conf)
args = [
@@ -642,7 +715,7 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir):
'--config', 'config.json',
'--datadir', str(testdatadir),
'--strategy-path', str(Path(__file__).parents[1] / 'strategy/strats'),
'--ticker-interval', '1m',
'--timeframe', '1m',
'--timerange', '1510694220-1510700340',
'--enable-position-stacking',
'--disable-max-market-positions',
@@ -654,12 +727,14 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir):
start_backtesting(args)
# 2 backtests, 4 tables
assert backtestmock.call_count == 2
assert gen_table_mock.call_count == 4
assert gen_strattable_mock.call_count == 1
assert text_table_mock.call_count == 4
assert strattable_mock.call_count == 1
assert sell_reason_mock.call_count == 2
assert strat_summary.call_count == 1
# check the logs, that will contain the backtest result
exists = [
'Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...',
'Parameter -i/--timeframe detected ... Using timeframe: 1m ...',
'Ignoring max_open_trades (--disable-max-market-positions was used) ...',
'Parameter --timerange detected: 1510694220-1510700340 ...',
f'Using data directory: {testdatadir} ...',
@@ -676,3 +751,92 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir):
for line in exists:
assert log_has(line, caplog)
@pytest.mark.filterwarnings("ignore:deprecated")
def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdatadir, capsys):
patch_exchange(mocker)
backtestmock = MagicMock(side_effect=[
pd.DataFrame({'pair': ['XRP/BTC', 'LTC/BTC'],
'profit_percent': [0.0, 0.0],
'profit_abs': [0.0, 0.0],
'open_time': pd.to_datetime(['2018-01-29 18:40:00',
'2018-01-30 03:30:00', ], utc=True
),
'close_time': pd.to_datetime(['2018-01-29 20:45:00',
'2018-01-30 05:35:00', ], utc=True),
'open_index': [78, 184],
'close_index': [125, 192],
'trade_duration': [235, 40],
'open_at_end': [False, False],
'open_rate': [0.104445, 0.10302485],
'close_rate': [0.104969, 0.103541],
'sell_reason': [SellType.ROI, SellType.ROI]
}),
pd.DataFrame({'pair': ['XRP/BTC', 'LTC/BTC', 'ETH/BTC'],
'profit_percent': [0.03, 0.01, 0.1],
'profit_abs': [0.01, 0.02, 0.2],
'open_time': pd.to_datetime(['2018-01-29 18:40:00',
'2018-01-30 03:30:00',
'2018-01-30 05:30:00'], utc=True
),
'close_time': pd.to_datetime(['2018-01-29 20:45:00',
'2018-01-30 05:35:00',
'2018-01-30 08:30:00'], utc=True),
'open_index': [78, 184, 185],
'close_index': [125, 224, 205],
'trade_duration': [47, 40, 20],
'open_at_end': [False, False, False],
'open_rate': [0.104445, 0.10302485, 0.122541],
'close_rate': [0.104969, 0.103541, 0.123541],
'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS]
}),
])
mocker.patch('freqtrade.pairlist.pairlistmanager.PairListManager.whitelist',
PropertyMock(return_value=['UNITTEST/BTC']))
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock)
patched_configuration_load_config_file(mocker, default_conf)
args = [
'backtesting',
'--config', 'config.json',
'--datadir', str(testdatadir),
'--strategy-path', str(Path(__file__).parents[1] / 'strategy/strats'),
'--timeframe', '1m',
'--timerange', '1510694220-1510700340',
'--enable-position-stacking',
'--disable-max-market-positions',
'--strategy-list',
'DefaultStrategy',
'TestStrategyLegacy',
]
args = get_args(args)
start_backtesting(args)
# check the logs, that will contain the backtest result
exists = [
'Parameter -i/--timeframe detected ... Using timeframe: 1m ...',
'Ignoring max_open_trades (--disable-max-market-positions was used) ...',
'Parameter --timerange detected: 1510694220-1510700340 ...',
f'Using data directory: {testdatadir} ...',
'Using stake_currency: BTC ...',
'Using stake_amount: 0.001 ...',
'Loading data from 2017-11-14T20:57:00+00:00 '
'up to 2017-11-14T22:58:00+00:00 (0 days)..',
'Backtesting with data from 2017-11-14T21:17:00+00:00 '
'up to 2017-11-14T22:58:00+00:00 (0 days)..',
'Parameter --enable-position-stacking detected ...',
'Running backtesting for Strategy DefaultStrategy',
'Running backtesting for Strategy TestStrategyLegacy',
]
for line in exists:
assert log_has(line, caplog)
captured = capsys.readouterr()
assert 'BACKTESTING REPORT' in captured.out
assert 'SELL REASON STATS' in captured.out
assert 'LEFT OPEN TRADES REPORT' in captured.out
assert 'STRATEGY SUMMARY' in captured.out

View File

@@ -29,7 +29,7 @@ def test_setup_optimize_configuration_without_arguments(mocker, default_conf, ca
assert 'pair_whitelist' in config['exchange']
assert 'datadir' in config
assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog)
assert 'ticker_interval' in config
assert 'timeframe' in config
assert not log_has_re('Parameter -i/--ticker-interval detected .*', caplog)
assert 'timerange' not in config
@@ -48,7 +48,7 @@ def test_setup_edge_configuration_with_arguments(mocker, edge_conf, caplog) -> N
'--config', 'config.json',
'--strategy', 'DefaultStrategy',
'--datadir', '/foo/bar',
'--ticker-interval', '1m',
'--timeframe', '1m',
'--timerange', ':100',
'--stoplosses=-0.01,-0.10,-0.001'
]
@@ -62,8 +62,8 @@ def test_setup_edge_configuration_with_arguments(mocker, edge_conf, caplog) -> N
assert 'datadir' in config
assert config['runmode'] == RunMode.EDGE
assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog)
assert 'ticker_interval' in config
assert log_has('Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...',
assert 'timeframe' in config
assert log_has('Parameter -i/--timeframe detected ... Using timeframe: 1m ...',
caplog)
assert 'timerange' in config

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()
@@ -93,7 +94,7 @@ def test_setup_hyperopt_configuration_without_arguments(mocker, default_conf, ca
assert 'pair_whitelist' in config['exchange']
assert 'datadir' in config
assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog)
assert 'ticker_interval' in config
assert 'timeframe' in config
assert not log_has_re('Parameter -i/--ticker-interval detected .*', caplog)
assert 'position_stacking' not in config
@@ -116,7 +117,7 @@ def test_setup_hyperopt_configuration_with_arguments(mocker, default_conf, caplo
'--config', 'config.json',
'--hyperopt', 'DefaultHyperOpt',
'--datadir', '/foo/bar',
'--ticker-interval', '1m',
'--timeframe', '1m',
'--timerange', ':100',
'--enable-position-stacking',
'--disable-max-market-positions',
@@ -135,8 +136,8 @@ def test_setup_hyperopt_configuration_with_arguments(mocker, default_conf, caplo
assert config['runmode'] == RunMode.HYPEROPT
assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog)
assert 'ticker_interval' in config
assert log_has('Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...',
assert 'timeframe' in config
assert log_has('Parameter -i/--timeframe detected ... Using timeframe: 1m ...',
caplog)
assert 'position_stacking' in config
@@ -196,7 +197,8 @@ def test_hyperoptresolver(mocker, default_conf, caplog) -> None:
"Using populate_sell_trend from the strategy.", caplog)
assert log_has("Hyperopt class does not provide populate_buy_trend() method. "
"Using populate_buy_trend from the strategy.", caplog)
assert hasattr(x, "ticker_interval")
assert hasattr(x, "ticker_interval") # DEPRECATED
assert hasattr(x, "timeframe")
def test_hyperoptresolver_wrongname(mocker, default_conf, caplog) -> None:
@@ -477,28 +479,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()
@@ -541,7 +545,7 @@ def test_start_calls_optimizer(mocker, default_conf, caplog, capsys) -> None:
)
patch_exchange(mocker)
# Co-test loading timeframe from strategy
del default_conf['ticker_interval']
del default_conf['timeframe']
default_conf.update({'config': 'config.json.example',
'hyperopt': 'DefaultHyperOpt',
'epochs': 1,
@@ -823,7 +827,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:

View File

@@ -1,17 +1,19 @@
from pathlib import Path
import pandas as pd
import pytest
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, store_backtest_result)
generate_pair_metrics, generate_edge_table, generate_sell_reason_stats,
text_table_bt_results, text_table_sell_reason, generate_strategy_metrics,
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):
def test_text_table_bt_results(default_conf, mocker):
results = pd.DataFrame(
{
@@ -35,12 +37,38 @@ def test_generate_text_table(default_conf, mocker):
'| TOTAL | 2 | 15.00 | 30.00 | 0.60000000 |'
' 15.00 | 0:20:00 | 2 | 0 | 0 |'
)
assert generate_text_table(data={'ETH/BTC': {}},
stake_currency='BTC', max_open_trades=2,
results=results) == result_str
pair_results = generate_pair_metrics(data={'ETH/BTC': {}}, stake_currency='BTC',
max_open_trades=2, results=results)
assert text_table_bt_results(pair_results, stake_currency='BTC') == result_str
def test_generate_text_table_sell_reason(default_conf, mocker):
def test_generate_pair_metrics(default_conf, mocker):
results = pd.DataFrame(
{
'pair': ['ETH/BTC', 'ETH/BTC'],
'profit_percent': [0.1, 0.2],
'profit_abs': [0.2, 0.4],
'trade_duration': [10, 30],
'wins': [2, 0],
'draws': [0, 0],
'losses': [0, 0]
}
)
pair_results = generate_pair_metrics(data={'ETH/BTC': {}}, stake_currency='BTC',
max_open_trades=2, results=results)
assert isinstance(pair_results, list)
assert len(pair_results) == 2
assert pair_results[-1]['key'] == 'TOTAL'
assert (
pytest.approx(pair_results[-1]['profit_mean_pct']) == pair_results[-1]['profit_mean'] * 100)
assert (
pytest.approx(pair_results[-1]['profit_sum_pct']) == pair_results[-1]['profit_sum'] * 100)
def test_text_table_sell_reason(default_conf):
results = pd.DataFrame(
{
@@ -65,11 +93,49 @@ 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(stake_currency='BTC', max_open_trades=2,
results=results) == result_str
sell_reason_stats = generate_sell_reason_stats(max_open_trades=2,
results=results)
assert text_table_sell_reason(sell_reason_stats=sell_reason_stats,
stake_currency='BTC') == result_str
def test_generate_text_table_strategy(default_conf, mocker):
def test_generate_sell_reason_stats(default_conf):
results = pd.DataFrame(
{
'pair': ['ETH/BTC', 'ETH/BTC', 'ETH/BTC'],
'profit_percent': [0.1, 0.2, -0.1],
'profit_abs': [0.2, 0.4, -0.2],
'trade_duration': [10, 30, 10],
'wins': [2, 0, 0],
'draws': [0, 0, 0],
'losses': [0, 0, 1],
'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS]
}
)
sell_reason_stats = generate_sell_reason_stats(max_open_trades=2,
results=results)
roi_result = sell_reason_stats[0]
assert roi_result['sell_reason'] == 'roi'
assert roi_result['trades'] == 2
assert pytest.approx(roi_result['profit_mean']) == 0.15
assert roi_result['profit_mean_pct'] == round(roi_result['profit_mean'] * 100, 2)
assert pytest.approx(roi_result['profit_mean']) == 0.15
assert roi_result['profit_mean_pct'] == round(roi_result['profit_mean'] * 100, 2)
stop_result = sell_reason_stats[1]
assert stop_result['sell_reason'] == 'stop_loss'
assert stop_result['trades'] == 1
assert pytest.approx(stop_result['profit_mean']) == -0.1
assert stop_result['profit_mean_pct'] == round(stop_result['profit_mean'] * 100, 2)
assert pytest.approx(stop_result['profit_mean']) == -0.1
assert stop_result['profit_mean_pct'] == round(stop_result['profit_mean'] * 100, 2)
def test_text_table_strategy(default_conf, mocker):
results = {}
results['TestStrategy1'] = pd.DataFrame(
{
@@ -106,7 +172,12 @@ def test_generate_text_table_strategy(default_conf, mocker):
'| TestStrategy2 | 3 | 30.00 | 90.00 | 1.30000000 |'
' 45.00 | 0:20:00 | 3 | 0 | 0 |'
)
assert generate_text_table_strategy('BTC', 2, all_results=results) == result_str
strategy_results = generate_strategy_metrics(stake_currency='BTC',
max_open_trades=2,
all_results=results)
assert text_table_strategy(strategy_results, 'BTC') == result_str
def test_generate_edge_table(edge_conf, mocker):

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")
@@ -21,7 +19,8 @@ def whitelist_conf(default_conf):
'TKN/BTC',
'TRST/BTC',
'SWT/BTC',
'BCC/BTC'
'BCC/BTC',
'HOT/BTC',
]
default_conf['exchange']['pair_blacklist'] = [
'BLK/BTC'
@@ -36,6 +35,53 @@ def whitelist_conf(default_conf):
return default_conf
@pytest.fixture(scope="function")
def whitelist_conf_2(default_conf):
default_conf['stake_currency'] = 'BTC'
default_conf['exchange']['pair_whitelist'] = [
'ETH/BTC', 'TKN/BTC', 'BLK/BTC', 'LTC/BTC',
'BTT/BTC', 'HOT/BTC', 'FUEL/BTC', 'XRP/BTC'
]
default_conf['exchange']['pair_blacklist'] = [
'BLK/BTC'
]
default_conf['pairlists'] = [
# { "method": "StaticPairList"},
{
"method": "VolumePairList",
"number_assets": 5,
"sort_key": "quoteVolume",
"refresh_period": 0,
},
]
return default_conf
@pytest.fixture(scope="function")
def whitelist_conf_3(default_conf):
default_conf['stake_currency'] = 'BTC'
default_conf['exchange']['pair_whitelist'] = [
'ETH/BTC', 'TKN/BTC', 'BLK/BTC', 'LTC/BTC',
'BTT/BTC', 'HOT/BTC', 'FUEL/BTC', 'XRP/BTC'
]
default_conf['exchange']['pair_blacklist'] = [
'BLK/BTC'
]
default_conf['pairlists'] = [
{
"method": "VolumePairList",
"number_assets": 5,
"sort_key": "quoteVolume",
"refresh_period": 0,
},
{
"method": "AgeFilter",
"min_days_listed": 2
}
]
return default_conf
@pytest.fixture(scope="function")
def static_pl_conf(whitelist_conf):
whitelist_conf['pairlists'] = [
@@ -55,7 +101,7 @@ def test_log_on_refresh(mocker, static_pl_conf, markets, tickers):
freqtrade = get_patched_freqtradebot(mocker, static_pl_conf)
logmock = MagicMock()
# Assign starting whitelist
pl = freqtrade.pairlists._pairlists[0]
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')
@@ -69,44 +115,44 @@ def test_log_on_refresh(mocker, static_pl_conf, markets, tickers):
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):
@@ -116,27 +162,52 @@ 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',
markets=PropertyMock(return_value=shitcoinmarkets),
)
)
# argument: use the whitelist dynamically by exchange-volume
whitelist = ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'XRP/BTC', 'HOT/BTC']
bot.pairlists.refresh_pairlist()
assert whitelist == bot.pairlists.whitelist
whitelist_conf['pairlists'] = [{'method': 'VolumePairList',
'config': {}
}
]
freqtrade.pairlists.refresh_pairlist()
assert whitelist == freqtrade.pairlists.whitelist
whitelist_conf['pairlists'] = [{'method': 'VolumePairList'}]
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_refresh_pairlist_dynamic_2(mocker, shitcoinmarkets, tickers, whitelist_conf_2):
tickers_dict = tickers()
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
exchange_has=MagicMock(return_value=True),
)
# Remove caching of ticker data to emulate changing volume by the time of second call
mocker.patch.multiple(
'freqtrade.pairlist.pairlistmanager.PairListManager',
_get_cached_tickers=MagicMock(return_value=tickers_dict),
)
freqtrade = get_patched_freqtradebot(mocker, whitelist_conf_2)
# Remock markets with shitcoinmarkets since get_patched_freqtradebot uses the markets fixture
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
markets=PropertyMock(return_value=shitcoinmarkets),
)
whitelist = ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'XRP/BTC', 'HOT/BTC']
freqtrade.pairlists.refresh_pairlist()
assert whitelist == freqtrade.pairlists.whitelist
whitelist = ['FUEL/BTC', 'ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'XRP/BTC']
tickers_dict['FUEL/BTC']['quoteVolume'] = 10000.0
freqtrade.pairlists.refresh_pairlist()
assert whitelist == freqtrade.pairlists.whitelist
def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf):
@@ -144,103 +215,231 @@ 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', 'ADAHALF/USDT']),
# No pair for ETH ...
"USDT", ['ETH/USDT', 'NANO/USDT', 'ADAHALF/USDT', 'ADADOUBLE/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": "AgeFilter", "min_days_listed": 2},
{"method": "PrecisionFilter"},
{"method": "PriceFilter", "low_price_ratio": 0.03},
{"method": "SpreadFilter", "max_spread_ratio": 0.005},
{"method": "ShuffleFilter"}],
"ETH", []),
# AgeFilter and VolumePairList (require 2 days only, all should pass age test)
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"},
{"method": "AgeFilter", "min_days_listed": 2}],
"BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'XRP/BTC', 'HOT/BTC']),
# AgeFilter and VolumePairList (require 10 days, all should fail age test)
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"},
{"method": "AgeFilter", "min_days_listed": 10}],
"BTC", []),
# Precisionfilter and quote volume
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"},
{"method": "PrecisionFilter"}], "BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'XRP/BTC']),
{"method": "PrecisionFilter"}],
"BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'XRP/BTC']),
# Precisionfilter bid
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "bidVolume"},
{"method": "PrecisionFilter"}], "BTC", ['FUEL/BTC', 'XRP/BTC', 'LTC/BTC', 'TKN/BTC']),
{"method": "PrecisionFilter"}],
"BTC", ['FUEL/BTC', 'XRP/BTC', 'LTC/BTC', 'TKN/BTC']),
# 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.
"USDT", ['ETH/USDT', 'NANO/USDT']),
# Hot is removed by precision_filter, Fuel by low_price_ratio, Ripple by min_price.
([{"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, "min_price": 0.01}],
"BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC']),
# Hot is removed by precision_filter, Fuel by low_price_ratio, Ethereum by max_price.
([{"method": "VolumePairList", "number_assets": 6, "sort_key": "quoteVolume"},
{"method": "PrecisionFilter"},
{"method": "PriceFilter", "low_price_ratio": 0.02, "max_price": 0.05}],
"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', 'HOT/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", ['HOT/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", ['ADADOUBLE/USDT', 'ETH/USDT', 'NANO/USDT', 'ADAHALF/USDT']),
# ShuffleFilter, other seed
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"},
{"method": "ShuffleFilter", "seed": 42}],
"USDT", ['ADAHALF/USDT', 'NANO/USDT', 'ADADOUBLE/USDT', 'ETH/USDT']),
# ShuffleFilter, no seed
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"},
{"method": "ShuffleFilter"}],
"USDT", 3), # whitelist_result is integer -- check only length of randomized pairlist
# AgeFilter only
([{"method": "AgeFilter", "min_days_listed": 2}],
"BTC", 'filter_at_the_beginning'), # OperationalException expected
# PrecisionFilter after StaticPairList
([{"method": "StaticPairList"},
{"method": "PrecisionFilter"}],
"BTC", ['ETH/BTC', 'TKN/BTC']),
# PrecisionFilter only
([{"method": "PrecisionFilter"}],
"BTC", 'filter_at_the_beginning'), # OperationalException expected
# PriceFilter after StaticPairList
([{"method": "StaticPairList"},
{"method": "PriceFilter", "low_price_ratio": 0.02, "min_price": 0.000001, "max_price": 0.1}],
"BTC", ['ETH/BTC', 'TKN/BTC']),
# PriceFilter only
([{"method": "PriceFilter", "low_price_ratio": 0.02}],
"BTC", 'filter_at_the_beginning'), # OperationalException expected
# ShuffleFilter after StaticPairList
([{"method": "StaticPairList"},
{"method": "ShuffleFilter", "seed": 42}],
"BTC", ['TKN/BTC', 'ETH/BTC', 'HOT/BTC']),
# ShuffleFilter only
([{"method": "ShuffleFilter", "seed": 42}],
"BTC", 'filter_at_the_beginning'), # OperationalException expected
# SpreadFilter after StaticPairList
([{"method": "StaticPairList"},
{"method": "SpreadFilter", "max_spread_ratio": 0.005}],
"BTC", ['ETH/BTC', 'TKN/BTC']),
# SpreadFilter only
([{"method": "SpreadFilter", "max_spread_ratio": 0.005}],
"BTC", 'filter_at_the_beginning'), # OperationalException expected
# Static Pairlist after VolumePairList, on a non-first position
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "bidVolume"},
{"method": "StaticPairList"}],
"BTC", 'static_in_the_middle'),
([{"method": "VolumePairList", "number_assets": 20, "sort_key": "quoteVolume"},
{"method": "PriceFilter", "low_price_ratio": 0.02}],
"USDT", ['ETH/USDT', 'NANO/USDT']),
])
def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, tickers,
pairlists, base_currency, whitelist_result,
caplog) -> None:
ohlcv_history_list, 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)
if whitelist_result == 'static_in_the_middle':
with pytest.raises(OperationalException,
match=r"StaticPairList can only be used in the first position "
r"in the list of Pairlist Handlers."):
freqtrade = get_patched_freqtradebot(mocker, whitelist_conf)
return
freqtrade = get_patched_freqtradebot(mocker, whitelist_conf)
mocker.patch.multiple('freqtrade.exchange.Exchange',
get_tickers=tickers,
markets=PropertyMock(return_value=shitcoinmarkets),
markets=PropertyMock(return_value=shitcoinmarkets)
)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
get_historic_ohlcv=MagicMock(return_value=ohlcv_history_list),
)
freqtrade.config['stake_currency'] = base_currency
freqtrade.pairlists.refresh_pairlist()
whitelist = freqtrade.pairlists.whitelist
# Set whitelist_result to None if pairlist is invalid and should produce exception
if whitelist_result == 'filter_at_the_beginning':
with pytest.raises(OperationalException,
match=r"This Pairlist Handler should not be used at the first position "
r"in the list of Pairlist Handlers."):
freqtrade.pairlists.refresh_pairlist()
else:
freqtrade.pairlists.refresh_pairlist()
whitelist = freqtrade.pairlists.whitelist
assert whitelist == whitelist_result
for pairlist in pairlists:
if pairlist['method'] == 'PrecisionFilter':
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) or
log_has_re(r"^Removed .* from whitelist, because ticker\['last'\] is empty.*",
caplog))
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'] == 'AgeFilter' and pairlist['min_days_listed'] and \
len(ohlcv_history_list) <= pairlist['min_days_listed']:
assert log_has_re(r'^Removed .* from whitelist, because age .* is less than '
r'.* day.*', caplog)
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' and whitelist_result:
assert (log_has_re(r'^Removed .* from whitelist, because 1 unit is .*%$', caplog) or
log_has_re(r'^Removed .* from whitelist, '
r'because last price < .*%$', caplog) or
log_has_re(r'^Removed .* from whitelist, '
r'because last price > .*%$', caplog) or
log_has_re(r"^Removed .* from whitelist, because ticker\['last'\] "
r"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_PrecisionFilter_error(mocker, whitelist_conf, tickers) -> None:
whitelist_conf['pairlists'] = [{"method": "StaticPairList"}, {"method": "PrecisionFilter"}]
del whitelist_conf['stoploss']
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
with pytest.raises(OperationalException,
match=r"PrecisionFilter can only work with stoploss defined\..*"):
PairListManager(MagicMock, whitelist_conf)
def test_gen_pair_whitelist_not_supported(mocker, default_conf, tickers) -> None:
default_conf['pairlists'] = [{'method': 'VolumePairList',
'config': {'number_assets': 10}
}]
default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}]
mocker.patch.multiple('freqtrade.exchange.Exchange',
get_tickers=tickers,
exchange_has=MagicMock(return_value=False),
)
with pytest.raises(OperationalException):
with pytest.raises(OperationalException,
match=r'Exchange does not support dynamic whitelist.*'):
get_patched_freqtradebot(mocker, default_conf)
@@ -283,12 +482,30 @@ 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
@pytest.mark.parametrize("pairlist", AVAILABLE_PAIRLISTS)
def test__whitelist_for_active_markets_empty(mocker, whitelist_conf, markets, pairlist, tickers):
whitelist_conf['pairlists'][0]['method'] = pairlist
mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True)
freqtrade = get_patched_freqtradebot(mocker, whitelist_conf)
mocker.patch.multiple('freqtrade.exchange.Exchange',
markets=PropertyMock(return_value=None),
get_tickers=tickers
)
# Assign starting whitelist
pairlist_handler = freqtrade.pairlists._pairlist_handlers[0]
with pytest.raises(OperationalException, match=r'Markets not loaded.*'):
pairlist_handler._whitelist_for_active_markets(['ETH/BTC'])
def test_volumepairlist_invalid_sortvalue(mocker, markets, whitelist_conf):
whitelist_conf['pairlists'][0].update({"sort_key": "asdf"})
@@ -305,18 +522,102 @@ 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_agefilter_min_days_listed_too_small(mocker, default_conf, markets, tickers, caplog):
default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10},
{'method': 'AgeFilter', 'min_days_listed': -1}]
mocker.patch.multiple('freqtrade.exchange.Exchange',
markets=PropertyMock(return_value=markets),
exchange_has=MagicMock(return_value=True),
get_tickers=tickers
)
with pytest.raises(OperationalException,
match=r'AgeFilter requires min_days_listed must be >= 1'):
get_patched_freqtradebot(mocker, default_conf)
def test_agefilter_min_days_listed_too_large(mocker, default_conf, markets, tickers, caplog):
default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10},
{'method': 'AgeFilter', 'min_days_listed': 99999}]
mocker.patch.multiple('freqtrade.exchange.Exchange',
markets=PropertyMock(return_value=markets),
exchange_has=MagicMock(return_value=True),
get_tickers=tickers
)
with pytest.raises(OperationalException,
match=r'AgeFilter requires min_days_listed must not exceed '
r'exchange max request size \([0-9]+\)'):
get_patched_freqtradebot(mocker, default_conf)
def test_agefilter_caching(mocker, markets, whitelist_conf_3, tickers, ohlcv_history_list):
mocker.patch.multiple('freqtrade.exchange.Exchange',
markets=PropertyMock(return_value=markets),
exchange_has=MagicMock(return_value=True),
get_tickers=tickers
)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
get_historic_ohlcv=MagicMock(return_value=ohlcv_history_list),
)
freqtrade = get_patched_freqtradebot(mocker, whitelist_conf_3)
assert freqtrade.exchange.get_historic_ohlcv.call_count == 0
freqtrade.pairlists.refresh_pairlist()
assert freqtrade.exchange.get_historic_ohlcv.call_count > 0
previous_call_count = freqtrade.exchange.get_historic_ohlcv.call_count
freqtrade.pairlists.refresh_pairlist()
# Should not have increased since first call.
assert freqtrade.exchange.get_historic_ohlcv.call_count == previous_call_count
@pytest.mark.parametrize("pairlistconfig,expected", [
({"method": "PriceFilter", "low_price_ratio": 0.001, "min_price": 0.00000010,
"max_price": 1.0}, "[{'PriceFilter': 'PriceFilter - Filtering pairs priced below "
"0.1% or below 0.00000010 or above 1.00000000.'}]"
),
({"method": "PriceFilter", "low_price_ratio": 0.001, "min_price": 0.00000010},
"[{'PriceFilter': 'PriceFilter - Filtering pairs priced below 0.1% or below 0.00000010.'}]"
),
({"method": "PriceFilter", "low_price_ratio": 0.001, "max_price": 1.00010000},
"[{'PriceFilter': 'PriceFilter - Filtering pairs priced below 0.1% or above 1.00010000.'}]"
),
({"method": "PriceFilter", "min_price": 0.00002000},
"[{'PriceFilter': 'PriceFilter - Filtering pairs priced below 0.00002000.'}]"
),
({"method": "PriceFilter"},
"[{'PriceFilter': 'PriceFilter - No price filters configured.'}]"
),
])
def test_pricefilter_desc(mocker, whitelist_conf, markets, pairlistconfig, expected):
mocker.patch.multiple('freqtrade.exchange.Exchange',
markets=PropertyMock(return_value=markets),
exchange_has=MagicMock(return_value=True)
)
whitelist_conf['pairlists'] = [pairlistconfig]
freqtrade = get_patched_freqtradebot(mocker, whitelist_conf)
short_desc = str(freqtrade.pairlists.short_desc())
assert short_desc == expected
def test_pairlistmanager_no_pairlist(mocker, markets, whitelist_conf, caplog):
@@ -325,5 +626,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

@@ -8,12 +8,13 @@ import pytest
from numpy import isnan
from freqtrade.edge import PairInfo
from freqtrade.exceptions import DependencyException, TemporaryError
from freqtrade.exceptions import ExchangeError, InvalidOrderException, TemporaryError
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, create_mock_trades
from tests.conftest import (create_mock_trades, get_patched_freqtradebot,
patch_get_signal)
# Functions for recurrent object patching
@@ -42,79 +43,133 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
rpc._rpc_trade_status()
freqtradebot.enter_positions()
trades = Trade.get_open_trades()
trades[0].open_order_id = None
freqtradebot.exit_positions(trades)
results = rpc._rpc_trade_status()
assert {
assert results[0] == {
'trade_id': 1,
'pair': 'ETH/BTC',
'base_currency': 'BTC',
'open_date': ANY,
'open_date_hum': ANY,
'open_timestamp': ANY,
'is_open': ANY,
'fee_open': ANY,
'fee_close': ANY,
'fee_open_cost': ANY,
'fee_open_currency': ANY,
'fee_close': fee.return_value,
'fee_close_cost': ANY,
'fee_close_currency': ANY,
'open_rate_requested': ANY,
'open_trade_price': ANY,
'open_trade_price': 0.0010025,
'close_rate_requested': ANY,
'sell_reason': ANY,
'sell_order_status': ANY,
'min_rate': ANY,
'max_rate': ANY,
'strategy': ANY,
'ticker_interval': ANY,
'timeframe': ANY,
'open_order_id': ANY,
'close_date': None,
'close_date_hum': None,
'close_timestamp': None,
'open_rate': 1.098e-05,
'close_rate': None,
'current_rate': 1.099e-05,
'amount': 91.07468124,
'amount': 91.07468123,
'amount_requested': 91.07468123,
'stake_amount': 0.001,
'close_profit': None,
'current_profit': -0.41,
'stop_loss': 0.0,
'initial_stop_loss': 0.0,
'initial_stop_loss_pct': None,
'stop_loss_pct': None,
'open_order': '(limit buy rem=0.00000000)'
} == results[0]
'close_profit_pct': None,
'close_profit_abs': None,
'current_profit': -0.00408133,
'current_profit_pct': -0.41,
'current_profit_abs': -4.09e-06,
'stop_loss': 9.882e-06,
'stop_loss_abs': 9.882e-06,
'stop_loss_pct': -10.0,
'stop_loss_ratio': -0.1,
'stoploss_order_id': None,
'stoploss_last_update': ANY,
'stoploss_last_update_timestamp': ANY,
'initial_stop_loss': 9.882e-06,
'initial_stop_loss_abs': 9.882e-06,
'initial_stop_loss_pct': -10.0,
'initial_stop_loss_ratio': -0.1,
'stoploss_current_dist': -1.1080000000000002e-06,
'stoploss_current_dist_ratio': -0.10081893,
'stoploss_entry_dist': -0.00010475,
'stoploss_entry_dist_ratio': -0.10448878,
'open_order': None,
'exchange': 'bittrex',
}
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate',
MagicMock(side_effect=DependencyException(f"Pair 'ETH/BTC' not available")))
MagicMock(side_effect=ExchangeError("Pair 'ETH/BTC' not available")))
results = rpc._rpc_trade_status()
assert isnan(results[0]['current_profit'])
assert isnan(results[0]['current_rate'])
assert {
assert results[0] == {
'trade_id': 1,
'pair': 'ETH/BTC',
'base_currency': 'BTC',
'open_date': ANY,
'open_date_hum': ANY,
'open_timestamp': ANY,
'is_open': ANY,
'fee_open': ANY,
'fee_close': ANY,
'fee_open_cost': ANY,
'fee_open_currency': ANY,
'fee_close': fee.return_value,
'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,
'timeframe': ANY,
'open_order_id': ANY,
'close_date': None,
'close_date_hum': None,
'close_timestamp': None,
'open_rate': 1.098e-05,
'close_rate': None,
'current_rate': ANY,
'amount': 91.07468124,
'amount': 91.07468123,
'amount_requested': 91.07468123,
'stake_amount': 0.001,
'close_profit': None,
'close_profit_pct': None,
'close_profit_abs': None,
'current_profit': ANY,
'stop_loss': 0.0,
'initial_stop_loss': 0.0,
'initial_stop_loss_pct': None,
'stop_loss_pct': None,
'open_order': '(limit buy rem=0.00000000)'
} == results[0]
'current_profit_pct': ANY,
'current_profit_abs': ANY,
'stop_loss': 9.882e-06,
'stop_loss_abs': 9.882e-06,
'stop_loss_pct': -10.0,
'stop_loss_ratio': -0.1,
'stoploss_order_id': None,
'stoploss_last_update': ANY,
'stoploss_last_update_timestamp': ANY,
'initial_stop_loss': 9.882e-06,
'initial_stop_loss_abs': 9.882e-06,
'initial_stop_loss_pct': -10.0,
'initial_stop_loss_ratio': -0.1,
'stoploss_current_dist': ANY,
'stoploss_current_dist_ratio': ANY,
'stoploss_entry_dist': -0.00010475,
'stoploss_entry_dist_ratio': -0.10448878,
'open_order': None,
'exchange': 'bittrex',
}
def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None:
@@ -157,7 +212,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=ExchangeError("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]
@@ -195,16 +250,18 @@ 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*'):
@@ -229,12 +286,66 @@ def test_rpc_trade_history(mocker, default_conf, markets, fee):
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'
assert len(trades['trades']) == 2
assert trades['trades_count'] == 2
# The first closed trade is for ETC ... sorting is descending
assert trades['trades'][-1]['pair'] == 'ETC/BTC'
assert trades['trades'][0]['pair'] == 'XRP/BTC'
def test_rpc_delete_trade(mocker, default_conf, fee, markets, caplog):
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
stoploss_mock = MagicMock()
cancel_mock = MagicMock()
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
markets=PropertyMock(return_value=markets),
cancel_order=cancel_mock,
cancel_stoploss_order=stoploss_mock,
)
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
freqtradebot.strategy.order_types['stoploss_on_exchange'] = True
create_mock_trades(fee)
rpc = RPC(freqtradebot)
with pytest.raises(RPCException, match='invalid argument'):
rpc._rpc_delete('200')
create_mock_trades(fee)
trades = Trade.query.all()
trades[1].stoploss_order_id = '1234'
trades[2].stoploss_order_id = '1234'
assert len(trades) > 2
res = rpc._rpc_delete('1')
assert isinstance(res, dict)
assert res['result'] == 'success'
assert res['trade_id'] == '1'
assert res['cancel_order_count'] == 1
assert cancel_mock.call_count == 1
assert stoploss_mock.call_count == 0
cancel_mock.reset_mock()
stoploss_mock.reset_mock()
res = rpc._rpc_delete('2')
assert isinstance(res, dict)
assert cancel_mock.call_count == 1
assert stoploss_mock.call_count == 1
assert res['cancel_order_count'] == 2
stoploss_mock = mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order',
side_effect=InvalidOrderException)
res = rpc._rpc_delete('3')
assert stoploss_mock.call_count == 1
stoploss_mock.reset_mock()
cancel_mock = mocker.patch('freqtrade.exchange.Exchange.cancel_order',
side_effect=InvalidOrderException)
res = rpc._rpc_delete('4')
assert cancel_mock.call_count == 1
assert stoploss_mock.call_count == 0
def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
@@ -259,8 +370,12 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
rpc = RPC(freqtradebot)
rpc._fiat_converter = CryptoToFiatConverter()
with pytest.raises(RPCException, match=r'.*no closed trade*'):
rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
res = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
assert res['trade_count'] == 0
assert res['first_trade_date'] == ''
assert res['first_trade_timestamp'] == 0
assert res['latest_trade_date'] == ''
assert res['latest_trade_timestamp'] == 0
# Create some test data
freqtradebot.enter_positions()
@@ -307,7 +422,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=ExchangeError("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'
@@ -536,7 +651,7 @@ def test_rpc_stopbuy(mocker, default_conf) -> None:
assert freqtradebot.config['max_open_trades'] != 0
result = rpc._rpc_stopbuy()
assert {'status': 'No more buy will occur from now. Run /reload_conf to reset.'} == result
assert {'status': 'No more buy will occur from now. Run /reload_config to reset.'} == result
assert freqtradebot.config['max_open_trades'] == 0
@@ -548,7 +663,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None:
'freqtrade.exchange.Exchange',
fetch_ticker=ticker,
cancel_order=cancel_order_mock,
get_order=MagicMock(
fetch_order=MagicMock(
return_value={
'status': 'closed',
'type': 'limit',
@@ -594,7 +709,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None:
trade = Trade.query.filter(Trade.id == '1').first()
filled_amount = trade.amount / 2
mocker.patch(
'freqtrade.exchange.Exchange.get_order',
'freqtrade.exchange.Exchange.fetch_order',
return_value={
'status': 'open',
'type': 'limit',
@@ -613,7 +728,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None:
amount = trade.amount
# make an limit-buy open trade, if there is no 'filled', don't sell it
mocker.patch(
'freqtrade.exchange.Exchange.get_order',
'freqtrade.exchange.Exchange.fetch_order',
return_value={
'status': 'open',
'type': 'limit',
@@ -630,7 +745,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None:
freqtradebot.enter_positions()
# make an limit-sell open trade
mocker.patch(
'freqtrade.exchange.Exchange.get_order',
'freqtrade.exchange.Exchange.fetch_order',
return_value={
'status': 'open',
'type': 'limit',
@@ -813,6 +928,20 @@ def test_rpc_blacklist(mocker, default_conf) -> None:
assert ret['blacklist'] == default_conf['exchange']['pair_blacklist']
assert ret['blacklist'] == ['DOGE/BTC', 'HOT/BTC', 'ETH/BTC']
ret = rpc._rpc_blacklist(["ETH/BTC"])
assert 'errors' in ret
assert isinstance(ret['errors'], dict)
assert ret['errors']['ETH/BTC']['error_msg'] == 'Pair ETH/BTC already in pairlist.'
ret = rpc._rpc_blacklist(["ETH/ETH"])
assert 'StaticPairList' in ret['method']
assert len(ret['blacklist']) == 3
assert ret['blacklist'] == default_conf['exchange']['pair_blacklist']
assert ret['blacklist'] == ['DOGE/BTC', 'HOT/BTC', 'ETH/BTC']
assert 'errors' in ret
assert isinstance(ret['errors'], dict)
assert ret['errors']['ETH/ETH']['error_msg'] == 'Pair ETH/ETH does not match stake currency.'
def test_rpc_edge_disabled(mocker, default_conf) -> None:
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())

View File

@@ -24,6 +24,7 @@ def botclient(default_conf, mocker):
default_conf.update({"api_server": {"enabled": True,
"listen_ip_address": "127.0.0.1",
"listen_port": 8080,
"CORS_origins": ['http://example.com'],
"username": _TEST_USER,
"password": _TEST_PASS,
}})
@@ -39,16 +40,28 @@ def client_post(client, url, data={}):
return client.post(url,
content_type="application/json",
data=data,
headers={'Authorization': _basic_auth_str(_TEST_USER, _TEST_PASS)})
headers={'Authorization': _basic_auth_str(_TEST_USER, _TEST_PASS),
'Origin': 'http://example.com'})
def client_get(client, url):
return client.get(url, headers={'Authorization': _basic_auth_str(_TEST_USER, _TEST_PASS)})
# Add fake Origin to ensure CORS kicks in
return client.get(url, headers={'Authorization': _basic_auth_str(_TEST_USER, _TEST_PASS),
'Origin': 'http://example.com'})
def assert_response(response, expected_code=200):
def client_delete(client, url):
# Add fake Origin to ensure CORS kicks in
return client.delete(url, headers={'Authorization': _basic_auth_str(_TEST_USER, _TEST_PASS),
'Origin': 'http://example.com'})
def assert_response(response, expected_code=200, needs_cors=True):
assert response.status_code == expected_code
assert response.content_type == "application/json"
if needs_cors:
assert ('Access-Control-Allow-Credentials', 'true') in response.headers._list
assert ('Access-Control-Allow-Origin', 'http://example.com') in response.headers._list
def test_api_not_found(botclient):
@@ -65,12 +78,12 @@ def test_api_not_found(botclient):
def test_api_unauthorized(botclient):
ftbot, client = botclient
rc = client.get(f"{BASE_URI}/ping")
assert_response(rc)
assert_response(rc, needs_cors=False)
assert rc.json == {'status': 'pong'}
# Don't send user/pass information
rc = client.get(f"{BASE_URI}/version")
assert_response(rc, 401)
assert_response(rc, 401, needs_cors=False)
assert rc.json == {'error': 'Unauthorized'}
# Change only username
@@ -94,6 +107,35 @@ 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"]}',
'Origin': 'http://example.com'})
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"]}',
'Origin': 'http://example.com'})
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 +165,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())
@@ -211,10 +259,10 @@ def test_api_cleanup(default_conf, mocker, caplog):
def test_api_reloadconf(botclient):
ftbot, client = botclient
rc = client_post(client, f"{BASE_URI}/reload_conf")
rc = client_post(client, f"{BASE_URI}/reload_config")
assert_response(rc)
assert rc.json == {'status': 'reloading config ...'}
assert ftbot.state == State.RELOAD_CONF
assert ftbot.state == State.RELOAD_CONFIG
def test_api_stopbuy(botclient):
@@ -223,7 +271,7 @@ def test_api_stopbuy(botclient):
rc = client_post(client, f"{BASE_URI}/stopbuy")
assert_response(rc)
assert rc.json == {'status': 'No more buy will occur from now. Run /reload_conf to reset.'}
assert rc.json == {'status': 'No more buy will occur from now. Run /reload_config to reset.'}
assert ftbot.config['max_open_trades'] == 0
@@ -283,7 +331,13 @@ 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['timeframe'] == '5m'
assert rc.json['timeframe_ms'] == 300000
assert rc.json['timeframe_min'] == 5
assert rc.json['state'] == 'running'
assert not rc.json['trailing_stop']
assert 'bid_strategy' in rc.json
assert 'ask_strategy' in rc.json
def test_api_daily(botclient, mocker, ticker, fee, markets):
@@ -298,11 +352,13 @@ 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):
def test_api_trades(botclient, mocker, fee, markets):
ftbot, client = botclient
patch_get_signal(ftbot, (True, False))
mocker.patch.multiple(
@@ -318,12 +374,53 @@ def test_api_trades(botclient, mocker, ticker, fee, markets):
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
rc = client_get(client, f"{BASE_URI}/trades?limit=1")
assert_response(rc)
assert len(rc.json['trades']) == 1
assert rc.json['trades_count'] == 1
def test_api_delete_trade(botclient, mocker, fee, markets):
ftbot, client = botclient
patch_get_signal(ftbot, (True, False))
stoploss_mock = MagicMock()
cancel_mock = MagicMock()
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
markets=PropertyMock(return_value=markets),
cancel_order=cancel_mock,
cancel_stoploss_order=stoploss_mock,
)
rc = client_delete(client, f"{BASE_URI}/trades/1")
# Error - trade won't exist yet.
assert_response(rc, 502)
create_mock_trades(fee)
ftbot.strategy.order_types['stoploss_on_exchange'] = True
trades = Trade.query.all()
trades[1].stoploss_order_id = '1234'
assert len(trades) > 2
rc = client_delete(client, f"{BASE_URI}/trades/1")
assert_response(rc)
assert rc.json['result_msg'] == 'Deleted trade 1. Closed 1 open orders.'
assert len(trades) - 1 == len(Trade.query.all())
assert cancel_mock.call_count == 1
cancel_mock.reset_mock()
rc = client_delete(client, f"{BASE_URI}/trades/1")
# Trade is gone now.
assert_response(rc, 502)
assert cancel_mock.call_count == 0
assert len(trades) - 1 == len(Trade.query.all())
rc = client_delete(client, f"{BASE_URI}/trades/2")
assert_response(rc)
assert rc.json['result_msg'] == 'Deleted trade 2. Closed 2 open orders.'
assert len(trades) - 2 == len(Trade.query.all())
assert stoploss_mock.call_count == 1
def test_api_edge_disabled(botclient, mocker, ticker, fee, markets):
@@ -353,9 +450,8 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, limit_buy_order, li
)
rc = client_get(client, f"{BASE_URI}/profit")
assert_response(rc, 502)
assert len(rc.json) == 1
assert rc.json == {"error": "Error querying _profit: no closed trade"}
assert_response(rc, 200)
assert rc.json['trade_count'] == 0
ftbot.enter_positions()
trade = Trade.query.first()
@@ -363,8 +459,11 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, limit_buy_order, li
# Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order)
rc = client_get(client, f"{BASE_URI}/profit")
assert_response(rc, 502)
assert rc.json == {"error": "Error querying _profit: no closed trade"}
assert_response(rc, 200)
# One open trade
assert rc.json['trade_count'] == 1
assert rc.json['best_pair'] == ''
assert rc.json['best_rate'] == 0
trade.update(limit_sell_order)
@@ -377,14 +476,27 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, limit_buy_order, li
'best_pair': 'ETH/BTC',
'best_rate': 6.2,
'first_trade_date': 'just now',
'first_trade_timestamp': ANY,
'latest_trade_date': 'just now',
'latest_trade_timestamp': ANY,
'profit_all_coin': 6.217e-05,
'profit_all_fiat': 0,
'profit_all_fiat': 0.76748865,
'profit_all_percent': 6.2,
'profit_all_percent_mean': 6.2,
'profit_all_ratio_mean': 0.06201058,
'profit_all_percent_sum': 6.2,
'profit_all_ratio_sum': 0.06201058,
'profit_closed_coin': 6.217e-05,
'profit_closed_fiat': 0,
'profit_closed_fiat': 0.76748865,
'profit_closed_percent': 6.2,
'trade_count': 1
'profit_closed_ratio_mean': 0.06201058,
'profit_closed_percent_mean': 6.2,
'profit_closed_ratio_sum': 0.06201058,
'profit_closed_percent_sum': 6.2,
'trade_count': 1,
'closed_trade_count': 1,
'winning_trades': 1,
'losing_trades': 0,
}
@@ -447,42 +559,72 @@ def test_api_status(botclient, mocker, ticker, fee, markets):
assert rc.json == []
ftbot.enter_positions()
trades = Trade.get_open_trades()
trades[0].open_order_id = None
ftbot.exit_positions(trades)
rc = client_get(client, f"{BASE_URI}/status")
assert_response(rc)
assert len(rc.json) == 1
assert rc.json == [{'amount': 91.07468124,
assert rc.json == [{'amount': 91.07468123,
'amount_requested': 91.07468123,
'base_currency': 'BTC',
'close_date': None,
'close_date_hum': None,
'close_timestamp': None,
'close_profit': None,
'close_profit_pct': None,
'close_profit_abs': None,
'close_rate': None,
'current_profit': -0.41,
'current_profit': -0.00408133,
'current_profit_pct': -0.41,
'current_profit_abs': -4.09e-06,
'current_rate': 1.099e-05,
'initial_stop_loss': 0.0,
'initial_stop_loss_pct': None,
'open_date': ANY,
'open_date_hum': 'just now',
'open_order': '(limit buy rem=0.00000000)',
'open_timestamp': ANY,
'open_order': None,
'open_rate': 1.098e-05,
'pair': 'ETH/BTC',
'stake_amount': 0.001,
'stop_loss': 0.0,
'stop_loss_pct': None,
'stop_loss': 9.882e-06,
'stop_loss_abs': 9.882e-06,
'stop_loss_pct': -10.0,
'stop_loss_ratio': -0.1,
'stoploss_order_id': None,
'stoploss_last_update': ANY,
'stoploss_last_update_timestamp': ANY,
'initial_stop_loss': 9.882e-06,
'initial_stop_loss_abs': 9.882e-06,
'initial_stop_loss_pct': -10.0,
'initial_stop_loss_ratio': -0.1,
'stoploss_current_dist': -1.1080000000000002e-06,
'stoploss_current_dist_ratio': -0.10081893,
'stoploss_entry_dist': -0.00010475,
'stoploss_entry_dist_ratio': -0.10448878,
'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,
'max_rate': 1.099e-05,
'min_rate': 1.098e-05,
'open_order_id': None,
'open_rate_requested': 1.098e-05,
'open_trade_price': 0.0010025,
'sell_reason': None,
'sell_order_status': None,
'strategy': 'DefaultStrategy',
'ticker_interval': 5}]
'ticker_interval': 5,
'timeframe': 5,
'exchange': 'bittrex',
}]
def test_api_version(botclient):
@@ -500,7 +642,9 @@ def test_api_blacklist(botclient, mocker):
assert_response(rc)
assert rc.json == {"blacklist": ["DOGE/BTC", "HOT/BTC"],
"length": 2,
"method": ["StaticPairList"]}
"method": ["StaticPairList"],
"errors": {},
}
# Add ETH/BTC to blacklist
rc = client_post(client, f"{BASE_URI}/blacklist",
@@ -508,7 +652,9 @@ def test_api_blacklist(botclient, mocker):
assert_response(rc)
assert rc.json == {"blacklist": ["DOGE/BTC", "HOT/BTC", "ETH/BTC"],
"length": 3,
"method": ["StaticPairList"]}
"method": ["StaticPairList"],
"errors": {},
}
def test_api_whitelist(botclient):
@@ -543,6 +689,7 @@ def test_api_forcebuy(botclient, mocker, fee):
fbuy_mock = MagicMock(return_value=Trade(
pair='ETH/ETH',
amount=1,
amount_requested=1,
exchange='bittrex',
stake_amount=1,
open_rate=0.245441,
@@ -559,32 +706,50 @@ def test_api_forcebuy(botclient, mocker, fee):
data='{"pair": "ETH/BTC"}')
assert_response(rc)
assert rc.json == {'amount': 1,
'amount_requested': 1,
'trade_id': None,
'close_date': None,
'close_date_hum': None,
'close_timestamp': None,
'close_rate': 0.265441,
'initial_stop_loss': None,
'initial_stop_loss_pct': None,
'open_date': ANY,
'open_date_hum': 'just now',
'open_timestamp': ANY,
'open_rate': 0.245441,
'pair': 'ETH/ETH',
'stake_amount': 1,
'stop_loss': None,
'stop_loss_abs': None,
'stop_loss_pct': None,
'trade_id': None,
'stop_loss_ratio': None,
'stoploss_order_id': None,
'stoploss_last_update': None,
'stoploss_last_update_timestamp': None,
'initial_stop_loss': None,
'initial_stop_loss_abs': None,
'initial_stop_loss_pct': None,
'initial_stop_loss_ratio': None,
'close_profit': None,
'close_profit_abs': 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,
'open_trade_price': 0.24605460,
'sell_reason': None,
'sell_order_status': None,
'strategy': None,
'ticker_interval': None
'ticker_interval': None,
'timeframe': None,
'exchange': 'bittrex',
}

View File

@@ -21,8 +21,9 @@ from freqtrade.rpc import RPCMessageType
from freqtrade.rpc.telegram import Telegram, authorized_only
from freqtrade.state import State
from freqtrade.strategy.interface import SellType
from tests.conftest import (get_patched_freqtradebot, log_has, patch_exchange,
patch_get_signal, patch_whitelist)
from tests.conftest import (create_mock_trades, get_patched_freqtradebot,
log_has, patch_exchange, patch_get_signal,
patch_whitelist)
class DummyCls(Telegram):
@@ -60,7 +61,7 @@ def test__init__(default_conf, mocker) -> None:
assert telegram._config == default_conf
def test_init(default_conf, mocker, caplog) -> None:
def test_telegram_init(default_conf, mocker, caplog) -> None:
start_polling = MagicMock()
mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock(return_value=start_polling))
@@ -71,10 +72,11 @@ def test_init(default_conf, mocker, caplog) -> None:
assert start_polling.dispatcher.add_handler.call_count > 0
assert start_polling.start_polling.call_count == 1
message_str = "rpc.telegram is listening for following commands: [['status'], ['profit'], " \
"['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], " \
"['performance'], ['daily'], ['count'], ['reload_conf'], ['show_config'], " \
"['stopbuy'], ['whitelist'], ['blacklist'], ['edge'], ['help'], ['version']]"
message_str = ("rpc.telegram is listening for following commands: [['status'], ['profit'], "
"['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], ['trades'], "
"['delete'], ['performance'], ['daily'], ['count'], ['reload_config', "
"'reload_conf'], ['show_config', 'show_conf'], ['stopbuy'], "
"['whitelist'], ['blacklist'], ['edge'], ['help'], ['version']]")
assert log_has(message_str, caplog)
@@ -166,10 +168,12 @@ def test_status(default_conf, update, mocker, fee, ticker,) -> None:
'current_rate': 1.098e-05,
'amount': 90.99181074,
'stake_amount': 90.99181074,
'close_profit': None,
'current_profit': -0.59,
'close_profit_pct': None,
'current_profit': -0.0059,
'current_profit_pct': -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)'
@@ -418,7 +422,7 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee,
telegram._profit(update=update, context=MagicMock())
assert msg_mock.call_count == 1
assert 'no closed trade' in msg_mock.call_args_list[0][0][0]
assert 'No trades yet.' in msg_mock.call_args_list[0][0][0]
msg_mock.reset_mock()
# Create some test data
@@ -430,7 +434,10 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee,
telegram._profit(update=update, context=MagicMock())
assert msg_mock.call_count == 1
assert 'no closed trade' in msg_mock.call_args_list[-1][0][0]
assert 'No closed trade' in msg_mock.call_args_list[-1][0][0]
assert '*ROI:* All trades' in msg_mock.call_args_list[-1][0][0]
assert ('∙ `-0.00000500 BTC (-0.50%) (-0.5 \N{GREEK CAPITAL LETTER SIGMA}%)`'
in msg_mock.call_args_list[-1][0][0])
msg_mock.reset_mock()
# Update the ticker with a market going up
@@ -442,11 +449,13 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee,
telegram._profit(update=update, context=MagicMock())
assert msg_mock.call_count == 1
assert '*ROI:* Close trades' in msg_mock.call_args_list[-1][0][0]
assert '∙ `0.00006217 BTC (6.20%)`' in msg_mock.call_args_list[-1][0][0]
assert '*ROI:* Closed trades' in msg_mock.call_args_list[-1][0][0]
assert ('∙ `0.00006217 BTC (6.20%) (6.2 \N{GREEK CAPITAL LETTER SIGMA}%)`'
in msg_mock.call_args_list[-1][0][0])
assert '∙ `0.933 USD`' in msg_mock.call_args_list[-1][0][0]
assert '*ROI:* All trades' in msg_mock.call_args_list[-1][0][0]
assert '∙ `0.00006217 BTC (6.20%)`' in msg_mock.call_args_list[-1][0][0]
assert ('∙ `0.00006217 BTC (6.20%) (6.2 \N{GREEK CAPITAL LETTER SIGMA}%)`'
in msg_mock.call_args_list[-1][0][0])
assert '∙ `0.933 USD`' in msg_mock.call_args_list[-1][0][0]
assert '*Best Performing:* `ETH/BTC: 6.20%`' in msg_mock.call_args_list[-1][0][0]
@@ -659,11 +668,11 @@ def test_stopbuy_handle(default_conf, update, mocker) -> None:
telegram._stopbuy(update=update, context=MagicMock())
assert freqtradebot.config['max_open_trades'] == 0
assert msg_mock.call_count == 1
assert 'No more buy will occur from now. Run /reload_conf to reset.' \
assert 'No more buy will occur from now. Run /reload_config to reset.' \
in msg_mock.call_args_list[0][0][0]
def test_reload_conf_handle(default_conf, update, mocker) -> None:
def test_reload_config_handle(default_conf, update, mocker) -> None:
msg_mock = MagicMock()
mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram',
@@ -676,14 +685,14 @@ def test_reload_conf_handle(default_conf, update, mocker) -> None:
freqtradebot.state = State.RUNNING
assert freqtradebot.state == State.RUNNING
telegram._reload_conf(update=update, context=MagicMock())
assert freqtradebot.state == State.RELOAD_CONF
telegram._reload_config(update=update, context=MagicMock())
assert freqtradebot.state == State.RELOAD_CONFIG
assert msg_mock.call_count == 1
assert 'reloading config' in msg_mock.call_args_list[0][0][0]
def test_forcesell_handle(default_conf, update, ticker, fee,
ticker_sell_up, mocker) -> None:
def test_telegram_forcesell_handle(default_conf, update, ticker, fee,
ticker_sell_up, mocker) -> None:
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
rpc_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock())
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
@@ -717,11 +726,12 @@ def test_forcesell_handle(default_conf, update, ticker, fee,
last_msg = rpc_mock.call_args_list[-1][0][0]
assert {
'type': RPCMessageType.SELL_NOTIFICATION,
'trade_id': 1,
'exchange': 'Bittrex',
'pair': 'ETH/BTC',
'gain': 'profit',
'limit': 1.173e-05,
'amount': 91.07468123861567,
'amount': 91.07468123,
'order_type': 'limit',
'open_rate': 1.098e-05,
'current_rate': 1.173e-05,
@@ -735,8 +745,8 @@ def test_forcesell_handle(default_conf, update, ticker, fee,
} == last_msg
def test_forcesell_down_handle(default_conf, update, ticker, fee,
ticker_sell_down, mocker) -> None:
def test_telegram_forcesell_down_handle(default_conf, update, ticker, fee,
ticker_sell_down, mocker) -> None:
mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price',
return_value=15000.0)
rpc_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock())
@@ -776,11 +786,12 @@ def test_forcesell_down_handle(default_conf, update, ticker, fee,
last_msg = rpc_mock.call_args_list[-1][0][0]
assert {
'type': RPCMessageType.SELL_NOTIFICATION,
'trade_id': 1,
'exchange': 'Bittrex',
'pair': 'ETH/BTC',
'gain': 'loss',
'limit': 1.043e-05,
'amount': 91.07468123861567,
'amount': 91.07468123,
'order_type': 'limit',
'open_rate': 1.098e-05,
'current_rate': 1.043e-05,
@@ -824,11 +835,12 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None
msg = rpc_mock.call_args_list[0][0][0]
assert {
'type': RPCMessageType.SELL_NOTIFICATION,
'trade_id': 1,
'exchange': 'Bittrex',
'pair': 'ETH/BTC',
'gain': 'loss',
'limit': 1.099e-05,
'amount': 91.07468123861567,
'amount': 91.07468123,
'order_type': 'limit',
'open_rate': 1.098e-05,
'current_rate': 1.099e-05,
@@ -1009,9 +1021,8 @@ def test_count_handle(default_conf, update, ticker, fee, mocker) -> None:
msg_mock.reset_mock()
telegram._count(update=update, context=MagicMock())
msg = '<pre> current max total stake\n--------- ----- -------------\n' \
' 1 {} {}</pre>'\
.format(
msg = ('<pre> current max total stake\n--------- ----- -------------\n'
' 1 {} {}</pre>').format(
default_conf['max_open_trades'],
default_conf['stake_amount']
)
@@ -1083,6 +1094,18 @@ def test_blacklist_static(default_conf, update, mocker) -> None:
in msg_mock.call_args_list[0][0][0])
assert freqtradebot.pairlists.blacklist == ["DOGE/BTC", "HOT/BTC", "ETH/BTC"]
msg_mock.reset_mock()
context = MagicMock()
context.args = ["ETH/ETH"]
telegram._blacklist(update=update, context=context)
assert msg_mock.call_count == 2
assert ("Error adding `ETH/ETH` to blacklist: `Pair ETH/ETH does not match stake currency.`"
in msg_mock.call_args_list[0][0][0])
assert ("Blacklist contains 3 pairs\n`DOGE/BTC, HOT/BTC, ETH/BTC`"
in msg_mock.call_args_list[1][0][0])
assert freqtradebot.pairlists.blacklist == ["DOGE/BTC", "HOT/BTC", "ETH/BTC"]
def test_edge_disabled(default_conf, update, mocker) -> None:
msg_mock = MagicMock()
@@ -1124,6 +1147,63 @@ def test_edge_enabled(edge_conf, update, mocker) -> None:
assert 'Pair Winrate Expectancy Stoploss' in msg_mock.call_args_list[0][0][0]
def test_telegram_trades(mocker, update, default_conf, fee):
msg_mock = MagicMock()
mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(),
_send_msg=msg_mock
)
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
telegram = Telegram(freqtradebot)
context = MagicMock()
context.args = []
telegram._trades(update=update, context=context)
assert "<b>0 recent trades</b>:" in msg_mock.call_args_list[0][0][0]
assert "<pre>" not in msg_mock.call_args_list[0][0][0]
msg_mock.reset_mock()
create_mock_trades(fee)
context = MagicMock()
context.args = [5]
telegram._trades(update=update, context=context)
msg_mock.call_count == 1
assert "2 recent trades</b>:" in msg_mock.call_args_list[0][0][0]
assert "Profit (" in msg_mock.call_args_list[0][0][0]
assert "Open Date" in msg_mock.call_args_list[0][0][0]
assert "<pre>" in msg_mock.call_args_list[0][0][0]
def test_telegram_delete_trade(mocker, update, default_conf, fee):
msg_mock = MagicMock()
mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(),
_send_msg=msg_mock
)
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
telegram = Telegram(freqtradebot)
context = MagicMock()
context.args = []
telegram._delete_trade(update=update, context=context)
assert "invalid argument" in msg_mock.call_args_list[0][0][0]
msg_mock.reset_mock()
create_mock_trades(fee)
context = MagicMock()
context.args = [1]
telegram._delete_trade(update=update, context=context)
msg_mock.call_count == 1
assert "Deleted trade 1." in msg_mock.call_args_list[0][0][0]
assert "Please make sure to take care of this asset" in msg_mock.call_args_list[0][0][0]
def test_help_handle(default_conf, update, mocker) -> None:
msg_mock = MagicMock()
mocker.patch.multiple(
@@ -1206,7 +1286,7 @@ def test_send_msg_buy_notification(default_conf, mocker) -> None:
'open_date': arrow.utcnow().shift(hours=-1)
})
assert msg_mock.call_args[0][0] \
== '*Bittrex:* Buying ETH/BTC\n' \
== '\N{LARGE BLUE CIRCLE} *Bittrex:* Buying ETH/BTC\n' \
'*Amount:* `1333.33333333`\n' \
'*Open Rate:* `0.00001099`\n' \
'*Current Rate:* `0.00001099`\n' \
@@ -1228,7 +1308,7 @@ def test_send_msg_buy_cancel_notification(default_conf, mocker) -> None:
'pair': 'ETH/BTC',
})
assert msg_mock.call_args[0][0] \
== ('*Bittrex:* Cancelling Open Buy Order for ETH/BTC')
== ('\N{WARNING SIGN} *Bittrex:* Cancelling Open Buy Order for ETH/BTC')
def test_send_msg_sell_notification(default_conf, mocker) -> None:
@@ -1261,7 +1341,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
'close_date': arrow.utcnow(),
})
assert msg_mock.call_args[0][0] \
== ('*Binance:* Selling KEY/ETH\n'
== ('\N{WARNING SIGN} *Binance:* Selling KEY/ETH\n'
'*Amount:* `1333.33333333`\n'
'*Open Rate:* `0.00007500`\n'
'*Current Rate:* `0.00003201`\n'
@@ -1289,7 +1369,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
'close_date': arrow.utcnow(),
})
assert msg_mock.call_args[0][0] \
== ('*Binance:* Selling KEY/ETH\n'
== ('\N{WARNING SIGN} *Binance:* Selling KEY/ETH\n'
'*Amount:* `1333.33333333`\n'
'*Open Rate:* `0.00007500`\n'
'*Current Rate:* `0.00003201`\n'
@@ -1319,7 +1399,8 @@ def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None:
'reason': 'Cancelled on exchange'
})
assert msg_mock.call_args[0][0] \
== ('*Binance:* Cancelling Open Sell Order for KEY/ETH. Reason: Cancelled on exchange')
== ('\N{WARNING SIGN} *Binance:* Cancelling Open Sell Order for KEY/ETH. '
'Reason: Cancelled on exchange')
msg_mock.reset_mock()
telegram.send_msg({
@@ -1329,7 +1410,7 @@ def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None:
'reason': 'timeout'
})
assert msg_mock.call_args[0][0] \
== ('*Binance:* Cancelling Open Sell Order for KEY/ETH. Reason: timeout')
== ('\N{WARNING SIGN} *Binance:* Cancelling Open Sell Order for KEY/ETH. Reason: timeout')
# Reset singleton function to avoid random breaks
telegram._fiat_converter.convert_amount = old_convamount
@@ -1363,7 +1444,7 @@ def test_warning_notification(default_conf, mocker) -> None:
'type': RPCMessageType.WARNING_NOTIFICATION,
'status': 'message'
})
assert msg_mock.call_args[0][0] == '*Warning:* `message`'
assert msg_mock.call_args[0][0] == '\N{WARNING SIGN} *Warning:* `message`'
def test_custom_notification(default_conf, mocker) -> None:
@@ -1421,12 +1502,11 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None:
'amount': 1333.3333333333335,
'open_date': arrow.utcnow().shift(hours=-1)
})
assert msg_mock.call_args[0][0] \
== '*Bittrex:* Buying ETH/BTC\n' \
'*Amount:* `1333.33333333`\n' \
'*Open Rate:* `0.00001099`\n' \
'*Current Rate:* `0.00001099`\n' \
'*Total:* `(0.001000 BTC)`'
assert msg_mock.call_args[0][0] == ('\N{LARGE BLUE CIRCLE} *Bittrex:* Buying ETH/BTC\n'
'*Amount:* `1333.33333333`\n'
'*Open Rate:* `0.00001099`\n'
'*Current Rate:* `0.00001099`\n'
'*Total:* `(0.001000 BTC)`')
def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None:
@@ -1457,15 +1537,37 @@ def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None:
'open_date': arrow.utcnow().shift(hours=-2, minutes=-35, seconds=-3),
'close_date': arrow.utcnow(),
})
assert msg_mock.call_args[0][0] \
== '*Binance:* Selling KEY/ETH\n' \
'*Amount:* `1333.33333333`\n' \
'*Open Rate:* `0.00007500`\n' \
'*Current Rate:* `0.00003201`\n' \
'*Close Rate:* `0.00003201`\n' \
'*Sell Reason:* `stop_loss`\n' \
'*Duration:* `2:35:03 (155.1 min)`\n' \
'*Profit:* `-57.41%`'
assert msg_mock.call_args[0][0] == ('\N{WARNING SIGN} *Binance:* Selling KEY/ETH\n'
'*Amount:* `1333.33333333`\n'
'*Open Rate:* `0.00007500`\n'
'*Current Rate:* `0.00003201`\n'
'*Close Rate:* `0.00003201`\n'
'*Sell Reason:* `stop_loss`\n'
'*Duration:* `2:35:03 (155.1 min)`\n'
'*Profit:* `-57.41%`')
@pytest.mark.parametrize('msg,expected', [
({'profit_percent': 20.1, 'sell_reason': 'roi'}, "\N{ROCKET}"),
({'profit_percent': 5.1, 'sell_reason': 'roi'}, "\N{ROCKET}"),
({'profit_percent': 2.56, 'sell_reason': 'roi'}, "\N{EIGHT SPOKED ASTERISK}"),
({'profit_percent': 1.0, 'sell_reason': 'roi'}, "\N{EIGHT SPOKED ASTERISK}"),
({'profit_percent': 0.0, 'sell_reason': 'roi'}, "\N{EIGHT SPOKED ASTERISK}"),
({'profit_percent': -5.0, 'sell_reason': 'stop_loss'}, "\N{WARNING SIGN}"),
({'profit_percent': -2.0, 'sell_reason': 'sell_signal'}, "\N{CROSS MARK}"),
])
def test__sell_emoji(default_conf, mocker, msg, expected):
del default_conf['fiat_display_currency']
msg_mock = MagicMock()
mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(),
_send_msg=msg_mock
)
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
telegram = Telegram(freqtradebot)
assert telegram._get_sell_emoji(msg) == expected
def test__send_msg(default_conf, mocker) -> None:

View File

@@ -29,7 +29,7 @@ class DefaultStrategy(IStrategy):
stoploss = -0.10
# Optimal ticker interval for the strategy
ticker_interval = '5m'
timeframe = '5m'
# Optional order type mapping
order_types = {

View File

@@ -31,6 +31,7 @@ class TestStrategyLegacy(IStrategy):
stoploss = -0.10
# Optimal ticker interval for the strategy
# Keep the legacy value here to test compatibility
ticker_interval = '5m'
def populate_indicators(self, dataframe: DataFrame) -> DataFrame:

View File

@@ -6,7 +6,7 @@ from .strats.default_strategy import DefaultStrategy
def test_default_strategy_structure():
assert hasattr(DefaultStrategy, 'minimal_roi')
assert hasattr(DefaultStrategy, 'stoploss')
assert hasattr(DefaultStrategy, 'ticker_interval')
assert hasattr(DefaultStrategy, 'timeframe')
assert hasattr(DefaultStrategy, 'populate_indicators')
assert hasattr(DefaultStrategy, 'populate_buy_trend')
assert hasattr(DefaultStrategy, 'populate_sell_trend')
@@ -18,7 +18,7 @@ def test_default_strategy(result):
metadata = {'pair': 'ETH/BTC'}
assert type(strategy.minimal_roi) is dict
assert type(strategy.stoploss) is float
assert type(strategy.ticker_interval) is str
assert type(strategy.timeframe) is str
indicators = strategy.populate_indicators(result, metadata)
assert type(indicators) is DataFrame
assert type(strategy.populate_buy_trend(indicators, metadata)) is DataFrame

View File

@@ -9,15 +9,18 @@ from pandas import DataFrame
from freqtrade.configuration import TimeRange
from freqtrade.data.history import load_data
from freqtrade.exceptions import DependencyException
from freqtrade.exceptions import StrategyError
from freqtrade.persistence import Trade
from freqtrade.resolvers import StrategyResolver
from tests.conftest import get_patched_exchange, log_has
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
from freqtrade.data.dataprovider import DataProvider
from tests.conftest import log_has, log_has_re
from .strats.default_strategy import DefaultStrategy
# Avoid to reinit the same object again and again
_STRATEGY = DefaultStrategy(config={})
_STRATEGY.dp = DataProvider({}, None, None)
def test_returns_latest_signal(mocker, default_conf, ohlcv_history):
@@ -28,63 +31,60 @@ def test_returns_latest_signal(mocker, default_conf, ohlcv_history):
mocked_history['buy'] = 0
mocked_history.loc[1, 'sell'] = 1
mocker.patch.object(
_STRATEGY, '_analyze_ticker_internal',
return_value=mocked_history
)
assert _STRATEGY.get_signal('ETH/BTC', '5m', ohlcv_history) == (False, True)
assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_history) == (False, True)
mocked_history.loc[1, 'sell'] = 0
mocked_history.loc[1, 'buy'] = 1
mocker.patch.object(
_STRATEGY, '_analyze_ticker_internal',
return_value=mocked_history
)
assert _STRATEGY.get_signal('ETH/BTC', '5m', ohlcv_history) == (True, False)
assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_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)
assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_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 candle (OHLCV) data for pair foo', caplog)
caplog.clear()
assert (False, False) == _STRATEGY.get_signal('bar', default_conf['ticker_interval'],
[])
assert log_has('Empty candle (OHLCV) data for pair bar', caplog)
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'],
ohlcv_history)
assert log_has('Unable to analyze candle (OHLCV) data for pair foo: xyz', caplog)
def test_get_signal_empty_dataframe(default_conf, mocker, caplog, ohlcv_history):
caplog.set_level(logging.INFO)
def test_analyze_pair_empty(default_conf, mocker, caplog, ohlcv_history):
mocker.patch.object(_STRATEGY.dp, 'ohlcv', return_value=ohlcv_history)
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'],
ohlcv_history)
assert log_has('Empty dataframe for pair xyz', caplog)
_STRATEGY.analyze_pair('ETH/BTC')
assert log_has('Empty dataframe for pair ETH/BTC', caplog)
def test_get_signal_empty(default_conf, mocker, caplog):
assert (False, False) == _STRATEGY.get_signal('foo', default_conf['timeframe'], DataFrame())
assert log_has('Empty candle (OHLCV) data for pair foo', caplog)
caplog.clear()
assert (False, False) == _STRATEGY.get_signal('bar', default_conf['timeframe'], None)
assert log_has('Empty candle (OHLCV) data for pair bar', caplog)
caplog.clear()
assert (False, False) == _STRATEGY.get_signal('baz', default_conf['timeframe'], DataFrame([]))
assert log_has('Empty candle (OHLCV) data for pair baz', caplog)
def test_get_signal_exception_valueerror(default_conf, mocker, caplog, ohlcv_history):
caplog.set_level(logging.INFO)
mocker.patch.object(_STRATEGY.dp, 'ohlcv', return_value=ohlcv_history)
mocker.patch.object(
_STRATEGY, '_analyze_ticker_internal',
side_effect=ValueError('xyz')
)
_STRATEGY.analyze_pair('foo')
assert log_has_re(r'Strategy caused the following exception: xyz.*', caplog)
caplog.clear()
mocker.patch.object(
_STRATEGY, 'analyze_ticker',
side_effect=Exception('invalid ticker history ')
)
_STRATEGY.analyze_pair('foo')
assert log_has_re(r'Strategy caused the following exception: xyz.*', caplog)
def test_get_signal_old_dataframe(default_conf, mocker, caplog, ohlcv_history):
@@ -98,13 +98,9 @@ def test_get_signal_old_dataframe(default_conf, mocker, caplog, ohlcv_history):
mocked_history.loc[1, 'buy'] = 1
caplog.set_level(logging.INFO)
mocker.patch.object(
_STRATEGY, '_analyze_ticker_internal',
return_value=mocked_history
)
mocker.patch.object(_STRATEGY, 'assert_df')
assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['ticker_interval'],
ohlcv_history)
assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['timeframe'], mocked_history)
assert log_has('Outdated history for pair xyz. Last tick is 16 minutes old', caplog)
@@ -119,42 +115,42 @@ def test_assert_df_raise(default_conf, mocker, caplog, ohlcv_history):
mocked_history.loc[1, 'buy'] = 1
caplog.set_level(logging.INFO)
mocker.patch.object(_STRATEGY.dp, 'ohlcv', return_value=ohlcv_history)
mocker.patch.object(_STRATEGY.dp, 'get_analyzed_dataframe', return_value=(mocked_history, 0))
mocker.patch.object(
_STRATEGY, 'assert_df',
side_effect=DependencyException('Dataframe returned...')
side_effect=StrategyError('Dataframe returned...')
)
assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['ticker_interval'],
ohlcv_history)
_STRATEGY.analyze_pair('xyz')
assert log_has('Unable to analyze candle (OHLCV) data for pair xyz: Dataframe returned...',
caplog)
def test_assert_df(default_conf, mocker, ohlcv_history):
def test_assert_df(default_conf, mocker, ohlcv_history, caplog):
# 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(DependencyException, match=r"Dataframe returned from strategy.*length\."):
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(DependencyException,
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(DependencyException,
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):
exchange = get_patched_exchange(mocker, default_conf)
mocker.patch.object(
_STRATEGY, 'analyze_ticker',
side_effect=Exception('invalid ticker history ')
)
assert _STRATEGY.get_signal(exchange, 'ETH/BTC', '5m') == (False, False)
_STRATEGY.disable_dataframe_checks = True
caplog.clear()
_STRATEGY.assert_df(ohlcv_history, len(ohlcv_history),
ohlcv_history.loc[1, 'close'], ohlcv_history.loc[0, 'date'])
assert log_has_re(r"Dataframe returned from strategy.*last date\.", caplog)
# reset to avoid problems in other tests due to test leakage
_STRATEGY.disable_dataframe_checks = False
def test_ohlcvdata_to_dataframe(default_conf, testdatadir) -> None:
@@ -333,6 +329,7 @@ def test__analyze_ticker_internal_skip_analyze(ohlcv_history, mocker, caplog) ->
)
strategy = DefaultStrategy({})
strategy.dp = DataProvider({}, None, None)
strategy.process_only_new_candles = True
ret = strategy._analyze_ticker_internal(ohlcv_history, {'pair': 'ETH/BTC'})
@@ -389,3 +386,51 @@ def test_is_pair_locked(default_conf):
pair = 'ETH/BTC'
strategy.unlock_pair(pair)
assert not strategy.is_pair_locked(pair)
def test_is_informative_pairs_callback(default_conf):
default_conf.update({'strategy': 'TestStrategyLegacy'})
strategy = StrategyResolver.load_strategy(default_conf)
# Should return empty
# Uses fallback to base implementation
assert [] == strategy.informative_pairs()
@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
caplog.clear()
# Test supressing error
ret = strategy_safe_wrapper(failing_method, message='DeadBeef', supress_error=True)()
assert log_has_re(r'DeadBeef.*', caplog)
@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

@@ -105,8 +105,9 @@ def test_strategy(result, default_conf):
assert strategy.stoploss == -0.10
assert default_conf['stoploss'] == -0.10
assert strategy.timeframe == '5m'
assert strategy.ticker_interval == '5m'
assert default_conf['ticker_interval'] == '5m'
assert default_conf['timeframe'] == '5m'
df_indicators = strategy.advise_indicators(result, metadata=metadata)
assert 'adx' in df_indicators
@@ -176,19 +177,19 @@ def test_strategy_override_trailing_stop_positive(caplog, default_conf):
caplog)
def test_strategy_override_ticker_interval(caplog, default_conf):
def test_strategy_override_timeframe(caplog, default_conf):
caplog.set_level(logging.INFO)
default_conf.update({
'strategy': 'DefaultStrategy',
'ticker_interval': 60,
'timeframe': 60,
'stake_currency': 'ETH'
})
strategy = StrategyResolver.load_strategy(default_conf)
assert strategy.ticker_interval == 60
assert strategy.timeframe == 60
assert strategy.stake_currency == 'ETH'
assert log_has("Override strategy 'ticker_interval' with value in config file: 60.",
assert log_has("Override strategy 'timeframe' with value in config file: 60.",
caplog)
@@ -357,8 +358,9 @@ def test_deprecate_populate_indicators(result, default_conf):
@pytest.mark.filterwarnings("ignore:deprecated")
def test_call_deprecated_function(result, monkeypatch, default_conf):
def test_call_deprecated_function(result, monkeypatch, default_conf, caplog):
default_location = Path(__file__).parent / "strats"
del default_conf['timeframe']
default_conf.update({'strategy': 'TestStrategyLegacy',
'strategy_path': default_location})
strategy = StrategyResolver.load_strategy(default_conf)
@@ -369,6 +371,8 @@ def test_call_deprecated_function(result, monkeypatch, default_conf):
assert strategy._buy_fun_len == 2
assert strategy._sell_fun_len == 2
assert strategy.INTERFACE_VERSION == 1
assert strategy.timeframe == '5m'
assert strategy.ticker_interval == '5m'
indicator_df = strategy.advise_indicators(result, metadata=metadata)
assert isinstance(indicator_df, DataFrame)
@@ -382,6 +386,9 @@ def test_call_deprecated_function(result, monkeypatch, default_conf):
assert isinstance(selldf, DataFrame)
assert 'sell' in selldf
assert log_has("DEPRECATED: Please migrate to using 'timeframe' instead of 'ticker_interval'.",
caplog)
def test_strategy_interface_versioning(result, monkeypatch, default_conf):
default_conf.update({'strategy': 'DefaultStrategy'})

View File

@@ -131,7 +131,7 @@ def test_parse_args_backtesting_custom() -> None:
assert call_args["verbosity"] == 0
assert call_args["command"] == 'backtesting'
assert call_args["func"] is not None
assert call_args["ticker_interval"] == '1m'
assert call_args["timeframe"] == '1m'
assert type(call_args["strategy_list"]) is list
assert len(call_args["strategy_list"]) == 2

View File

@@ -73,7 +73,7 @@ def test_load_config_file_error(default_conf, mocker, caplog) -> None:
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=f".*Please verify the following segment.*"):
with pytest.raises(OperationalException, match=r".*Please verify the following segment.*"):
load_config_file('somefile')
@@ -87,7 +87,7 @@ def test_load_config_file_error_range(default_conf, mocker, caplog) -> None:
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, ')
'"timeframe": "5m", "dry_run": true, "cance')
def test__args_to_config(caplog):
@@ -401,8 +401,8 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) ->
assert 'datadir' in config
assert 'user_data_dir' in config
assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog)
assert 'ticker_interval' in config
assert not log_has('Parameter -i/--ticker-interval detected ...', caplog)
assert 'timeframe' in config
assert not log_has('Parameter -i/--timeframe detected ...', caplog)
assert 'position_stacking' not in config
assert not log_has('Parameter --enable-position-stacking detected ...', caplog)
@@ -448,8 +448,8 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
assert log_has('Using user-data directory: {} ...'.format(Path("/tmp/freqtrade")), caplog)
assert 'user_data_dir' in config
assert 'ticker_interval' in config
assert log_has('Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...',
assert 'timeframe' in config
assert log_has('Parameter -i/--timeframe detected ... Using timeframe: 1m ...',
caplog)
assert 'position_stacking' in config
@@ -494,8 +494,8 @@ def test_setup_configuration_with_stratlist(mocker, default_conf, caplog) -> Non
assert 'pair_whitelist' in config['exchange']
assert 'datadir' in config
assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog)
assert 'ticker_interval' in config
assert log_has('Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...',
assert 'timeframe' in config
assert log_has('Parameter -i/--timeframe detected ... Using timeframe: 1m ...',
caplog)
assert 'strategy_list' in config
@@ -654,12 +654,14 @@ def test_set_loggers() -> None:
assert logging.getLogger('requests').level is logging.DEBUG
assert logging.getLogger('ccxt.base.exchange').level is logging.INFO
assert logging.getLogger('telegram').level is logging.INFO
assert logging.getLogger('werkzeug').level is logging.INFO
_set_loggers(verbosity=3)
_set_loggers(verbosity=3, api_verbosity='error')
assert logging.getLogger('requests').level is logging.DEBUG
assert logging.getLogger('ccxt.base.exchange').level is logging.DEBUG
assert logging.getLogger('telegram').level is logging.INFO
assert logging.getLogger('werkzeug').level is logging.ERROR
@pytest.mark.skipif(sys.platform == "win32", reason="does not run on windows")
@@ -869,6 +871,14 @@ def test_load_config_default_exchange_name(all_conf) -> None:
validate_config_schema(all_conf)
def test_load_config_stoploss_exchange_limit_ratio(all_conf) -> None:
all_conf['order_types']['stoploss_on_exchange_limit_ratio'] = 1.15
with pytest.raises(ValidationError,
match=r"1.15 is greater than the maximum"):
validate_config_schema(all_conf)
@pytest.mark.parametrize("keys", [("exchange", "sandbox", False),
("exchange", "key", ""),
("exchange", "secret", ""),
@@ -1041,18 +1051,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': {
@@ -1060,8 +1058,9 @@ def test_process_deprecated_setting_edge(mocker, edge_conf, caplog):
'capital_available_percentage': 0.5,
}})
process_temporary_deprecated_settings(edge_conf)
assert log_has_re(r"DEPRECATED.*Using 'edge.capital_available_percentage'*", caplog)
with pytest.raises(OperationalException,
match=r"DEPRECATED.*Using 'edge.capital_available_percentage'*"):
process_temporary_deprecated_settings(edge_conf)
def test_check_conflicting_settings(mocker, default_conf, caplog):
@@ -1149,3 +1148,25 @@ def test_process_deprecated_setting(mocker, default_conf, caplog):
'sectionB', 'deprecated_setting')
assert not log_has_re('DEPRECATED', caplog)
assert default_conf['sectionA']['new_setting'] == 'valA'
def test_process_deprecated_ticker_interval(mocker, default_conf, caplog):
message = "DEPRECATED: Please use 'timeframe' instead of 'ticker_interval."
config = deepcopy(default_conf)
process_temporary_deprecated_settings(config)
assert not log_has(message, caplog)
del config['timeframe']
config['ticker_interval'] = '15m'
process_temporary_deprecated_settings(config)
assert log_has(message, caplog)
assert config['ticker_interval'] == '15m'
config = deepcopy(default_conf)
# Have both timeframe and ticker interval in config
# Can also happen when using ticker_interval in configuration, and --timeframe as cli argument
config['timeframe'] = '5m'
config['ticker_interval'] = '4h'
with pytest.raises(OperationalException,
match=r"Both 'timeframe' and 'ticker_interval' detected."):
process_temporary_deprecated_settings(config)

View File

@@ -2,7 +2,8 @@
# Test Documentation boxes -
# !!! <TYPE>: is not allowed!
# !!! <TYPE> "title" - Title needs to be quoted!
grep -Er '^!{3}\s\S+:|^!{3}\s\S+\s[^"]' docs/*
# !!! <TYPE> Spaces at the beginning are not allowed
grep -Er '^!{3}\s\S+:|^!{3}\s\S+\s[^"]|^\s+!{3}\s\S+' docs/*
if [ $? -ne 0 ]; then
echo "Docs test success."

File diff suppressed because it is too large Load Diff

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])
@@ -60,14 +62,13 @@ def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee,
get_fee=fee,
amount_to_precision=lambda s, x, y: y,
price_to_precision=lambda s, x, y: y,
get_order=stoploss_order_mock,
cancel_order=cancel_order_mock,
fetch_stoploss_order=stoploss_order_mock,
cancel_stoploss_order=cancel_order_mock,
)
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)
@@ -78,10 +79,15 @@ def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee,
freqtrade.strategy.order_types['stoploss_on_exchange'] = True
# Switch ordertype to market to close trade immediately
freqtrade.strategy.order_types['sell'] = 'market'
freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=True)
freqtrade.strategy.confirm_trade_exit = MagicMock(return_value=True)
patch_get_signal(freqtrade)
# Create some test data
freqtrade.enter_positions()
assert freqtrade.strategy.confirm_trade_entry.call_count == 3
freqtrade.strategy.confirm_trade_entry.reset_mock()
assert freqtrade.strategy.confirm_trade_exit.call_count == 0
wallets_mock.reset_mock()
Trade.session = MagicMock()
@@ -94,11 +100,15 @@ def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee,
n = freqtrade.exit_positions(trades)
assert n == 2
assert should_sell_mock.call_count == 2
assert freqtrade.strategy.confirm_trade_entry.call_count == 0
assert freqtrade.strategy.confirm_trade_exit.call_count == 1
freqtrade.strategy.confirm_trade_exit.reset_mock()
# 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 +154,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

@@ -35,12 +35,12 @@ def test_parse_args_backtesting(mocker) -> None:
main(['backtesting'])
assert backtesting_mock.call_count == 1
call_args = backtesting_mock.call_args[0][0]
assert call_args["config"] == ['config.json']
assert call_args["verbosity"] == 0
assert call_args["command"] == 'backtesting'
assert call_args["func"] is not None
assert callable(call_args["func"])
assert call_args["ticker_interval"] is None
assert call_args['config'] == ['config.json']
assert call_args['verbosity'] == 0
assert call_args['command'] == 'backtesting'
assert call_args['func'] is not None
assert callable(call_args['func'])
assert call_args['timeframe'] is None
def test_main_start_hyperopt(mocker) -> None:
@@ -115,12 +115,38 @@ def test_main_operational_exception(mocker, default_conf, caplog) -> None:
assert log_has('Oh snap!', caplog)
def test_main_reload_conf(mocker, default_conf, caplog) -> None:
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_config(mocker, default_conf, caplog) -> None:
patch_exchange(mocker)
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.cleanup', MagicMock())
# Simulate Running, reload, running workflow
worker_mock = MagicMock(side_effect=[State.RUNNING,
State.RELOAD_CONF,
State.RELOAD_CONFIG,
State.RUNNING,
OperationalException("Oh snap!")])
mocker.patch('freqtrade.worker.Worker._worker', worker_mock)

View File

@@ -9,7 +9,9 @@ import pytest
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, safe_value_fallback, shorten_date)
plural, render_template,
render_template_with_fallback, safe_value_fallback,
safe_value_fallback2, shorten_date)
def test_shorten_date() -> None:
@@ -94,24 +96,40 @@ def test_format_ms_time() -> None:
def test_safe_value_fallback():
dict1 = {'keya': None, 'keyb': 2, 'keyc': 5, 'keyd': None}
assert safe_value_fallback(dict1, 'keya', 'keyb') == 2
assert safe_value_fallback(dict1, 'keyb', 'keya') == 2
assert safe_value_fallback(dict1, 'keyb', 'keyc') == 2
assert safe_value_fallback(dict1, 'keya', 'keyc') == 5
assert safe_value_fallback(dict1, 'keyc', 'keyb') == 5
assert safe_value_fallback(dict1, 'keya', 'keyd') is None
assert safe_value_fallback(dict1, 'keyNo', 'keyNo') is None
assert safe_value_fallback(dict1, 'keyNo', 'keyNo', 55) == 55
def test_safe_value_fallback2():
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_fallback2(dict1, dict2, 'keya', 'keya') == 20
assert safe_value_fallback2(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_fallback2(dict1, dict2, 'keyb', 'keyb') == 2
assert safe_value_fallback2(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_fallback2(dict1, dict2, 'keyc', 'keyc') == 5
assert safe_value_fallback2(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_fallback2(dict1, dict2, 'keyd', 'keyd') is None
assert safe_value_fallback2(dict2, dict1, 'keyd', 'keyd') is None
assert safe_value_fallback2(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
assert safe_value_fallback2(dict1, dict2, 'keyNo', 'keyNo') is None
assert safe_value_fallback2(dict2, dict1, 'keyNo', 'keyNo') is None
assert safe_value_fallback2(dict2, dict1, 'keyNo', 'keyNo', 1234) == 1234
def test_plural() -> None:
@@ -144,3 +162,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

@@ -298,7 +298,7 @@ def test_calc_profit(limit_buy_order, limit_sell_order, fee):
fee_close=fee.return_value,
exchange='bittrex',
)
trade.open_order_id = 'profit_percent'
trade.open_order_id = 'something'
trade.update(limit_buy_order) # Buy @ 0.00001099
# Custom closing rate and regular fee rate
@@ -332,7 +332,7 @@ def test_calc_profit_ratio(limit_buy_order, limit_sell_order, fee):
fee_close=fee.return_value,
exchange='bittrex',
)
trade.open_order_id = 'profit_percent'
trade.open_order_id = 'something'
trade.update(limit_buy_order) # Buy @ 0.00001099
# Get percent of profit with a custom rate (Higher than open rate)
@@ -457,6 +457,7 @@ def test_migrate_old(mocker, default_conf, fee):
assert trade.close_rate_requested is None
assert trade.is_open == 1
assert trade.amount == amount
assert trade.amount_requested == amount
assert trade.stake_amount == default_conf.get("stake_amount")
assert trade.pair == "ETC/BTC"
assert trade.exchange == "bittrex"
@@ -465,6 +466,11 @@ def test_migrate_old(mocker, default_conf, fee):
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
assert trade.timeframe is None
trade = Trade.query.filter(Trade.id == 2).first()
assert trade.close_rate is not None
@@ -473,6 +479,7 @@ def test_migrate_old(mocker, default_conf, fee):
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):
@@ -507,11 +514,11 @@ def test_migrate_new(mocker, default_conf, fee, caplog):
);"""
insert_table_old = """INSERT INTO trades (exchange, pair, is_open, fee,
open_rate, stake_amount, amount, open_date,
stop_loss, initial_stop_loss, max_rate)
stop_loss, initial_stop_loss, max_rate, ticker_interval)
VALUES ('binance', 'ETC/BTC', 1, {fee},
0.00258580, {stake}, {amount},
'2019-11-28 12:44:24.000000',
0.0, 0.0, 0.0)
0.0, 0.0, 0.0, '5m')
""".format(fee=fee.return_value,
stake=default_conf.get("stake_amount"),
amount=amount
@@ -540,6 +547,7 @@ def test_migrate_new(mocker, default_conf, fee, caplog):
assert trade.close_rate_requested is None
assert trade.is_open == 1
assert trade.amount == amount
assert trade.amount_requested == amount
assert trade.stake_amount == default_conf.get("stake_amount")
assert trade.pair == "ETC/BTC"
assert trade.exchange == "binance"
@@ -549,7 +557,7 @@ def test_migrate_new(mocker, default_conf, fee, caplog):
assert trade.initial_stop_loss == 0.0
assert trade.sell_reason is None
assert trade.strategy is None
assert trade.ticker_interval is None
assert trade.timeframe == '5m'
assert trade.stoploss_order_id is None
assert trade.stoploss_last_update is None
assert log_has("trying trades_bak1", caplog)
@@ -719,6 +727,7 @@ def test_to_json(default_conf, fee):
pair='ETH/BTC',
stake_amount=0.001,
amount=123.0,
amount_requested=123.0,
fee_open=fee.return_value,
fee_close=fee.return_value,
open_date=arrow.utcnow().shift(hours=-2).datetime,
@@ -734,34 +743,54 @@ def test_to_json(default_conf, fee):
'is_open': None,
'open_date_hum': '2 hours ago',
'open_date': trade.open_date.strftime("%Y-%m-%d %H:%M:%S"),
'open_timestamp': int(trade.open_date.timestamp() * 1000),
'open_order_id': 'dry_run_buy_12345',
'close_date_hum': None,
'close_date': None,
'close_timestamp': 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,
'amount_requested': 123.0,
'stake_amount': 0.001,
'close_profit': None,
'close_profit_abs': None,
'sell_reason': None,
'sell_order_status': None,
'stop_loss': None,
'stop_loss_abs': None,
'stop_loss_ratio': None,
'stop_loss_pct': None,
'stoploss_order_id': None,
'stoploss_last_update': None,
'stoploss_last_update_timestamp': None,
'initial_stop_loss': None,
'initial_stop_loss_abs': None,
'initial_stop_loss_pct': None,
'initial_stop_loss_ratio': None,
'min_rate': None,
'max_rate': None,
'strategy': None,
'ticker_interval': None}
'ticker_interval': None,
'timeframe': None,
'exchange': 'bittrex',
}
# Simulate dry_run entries
trade = Trade(
pair='XRP/BTC',
stake_amount=0.001,
amount=100.0,
amount_requested=101.0,
fee_open=fee.return_value,
fee_close=fee.return_value,
open_date=arrow.utcnow().shift(hours=-2).datetime,
@@ -777,20 +806,35 @@ def test_to_json(default_conf, fee):
'pair': 'XRP/BTC',
'open_date_hum': '2 hours ago',
'open_date': trade.open_date.strftime("%Y-%m-%d %H:%M:%S"),
'open_timestamp': int(trade.open_date.timestamp() * 1000),
'close_date_hum': 'an hour ago',
'close_date': trade.close_date.strftime("%Y-%m-%d %H:%M:%S"),
'close_timestamp': int(trade.close_date.timestamp() * 1000),
'open_rate': 0.123,
'close_rate': 0.125,
'amount': 100.0,
'amount_requested': 101.0,
'stake_amount': 0.001,
'stop_loss': None,
'stop_loss_abs': None,
'stop_loss_pct': None,
'stop_loss_ratio': None,
'stoploss_order_id': None,
'stoploss_last_update': None,
'stoploss_last_update_timestamp': None,
'initial_stop_loss': None,
'initial_stop_loss_abs': None,
'initial_stop_loss_pct': None,
'initial_stop_loss_ratio': None,
'close_profit': None,
'close_profit_abs': 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,
@@ -798,8 +842,12 @@ def test_to_json(default_conf, fee):
'open_rate_requested': None,
'open_trade_price': 12.33075,
'sell_reason': None,
'sell_order_status': None,
'strategy': None,
'ticker_interval': None}
'ticker_interval': None,
'timeframe': None,
'exchange': 'bittrex',
}
def test_stoploss_reinitialization(default_conf, fee):
@@ -862,6 +910,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):
@@ -878,7 +995,7 @@ def test_get_overall_performance(fee):
create_mock_trades(fee)
res = Trade.get_overall_performance()
assert len(res) == 1
assert len(res) == 2
assert 'pair' in res[0]
assert 'profit' in res[0]
assert 'count' in res[0]
@@ -893,5 +1010,5 @@ def test_get_best_pair(fee):
create_mock_trades(fee)
res = Trade.get_best_pair()
assert len(res) == 2
assert res[0] == 'ETC/BTC'
assert res[1] == 0.005
assert res[0] == 'XRP/BTC'
assert res[1] == 0.01

View File

@@ -21,7 +21,7 @@ from freqtrade.plot.plotting import (add_indicators, add_profit,
load_and_plot_trades, plot_profit,
plot_trades, store_plot_file)
from freqtrade.resolvers import StrategyResolver
from tests.conftest import get_args, log_has, log_has_re
from tests.conftest import get_args, log_has, log_has_re, patch_exchange
def fig_generating_mock(fig, *args, **kwargs):
@@ -47,7 +47,7 @@ def generate_empty_figure():
def test_init_plotscript(default_conf, mocker, testdatadir):
default_conf['timerange'] = "20180110-20180112"
default_conf['trade_source'] = "file"
default_conf['ticker_interval'] = "5m"
default_conf['timeframe'] = "5m"
default_conf["datadir"] = testdatadir
default_conf['exportfilename'] = testdatadir / "backtest-result_test.json"
ret = init_plotscript(default_conf)
@@ -124,7 +124,7 @@ def test_plot_trades(testdatadir, caplog):
trade_sell = find_trace_in_fig_data(figure.data, 'Sell - Profit')
assert isinstance(trade_sell, go.Scatter)
assert trade_sell.yaxis == 'y'
assert len(trades.loc[trades['profitperc'] > 0]) == len(trade_sell.x)
assert len(trades.loc[trades['profit_percent'] > 0]) == len(trade_sell.x)
assert trade_sell.marker.color == 'green'
assert trade_sell.marker.symbol == 'square-open'
assert trade_sell.text[0] == '4.0%, roi, 15 min'
@@ -132,7 +132,7 @@ def test_plot_trades(testdatadir, caplog):
trade_sell_loss = find_trace_in_fig_data(figure.data, 'Sell - Loss')
assert isinstance(trade_sell_loss, go.Scatter)
assert trade_sell_loss.yaxis == 'y'
assert len(trades.loc[trades['profitperc'] <= 0]) == len(trade_sell_loss.x)
assert len(trades.loc[trades['profit_percent'] <= 0]) == len(trade_sell_loss.x)
assert trade_sell_loss.marker.color == 'red'
assert trade_sell_loss.marker.symbol == 'square-open'
assert trade_sell_loss.text[5] == '-10.4%, stop_loss, 720 min'
@@ -316,6 +316,8 @@ def test_start_plot_dataframe(mocker):
def test_load_and_plot_trades(default_conf, mocker, caplog, testdatadir):
patch_exchange(mocker)
default_conf['trade_source'] = 'file'
default_conf["datadir"] = testdatadir
default_conf['exportfilename'] = testdatadir / "backtest-result_test.json"
@@ -374,7 +376,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'] = testdatadir / "backtest-result_test.json"
default_conf['exportfilename'] = testdatadir / "backtest-result_test_nofile.json"
default_conf['pairs'] = ["ETH/BTC", "LTC/BTC"]
profit_mock = MagicMock()
@@ -384,6 +386,12 @@ def test_plot_profit(default_conf, mocker, testdatadir, caplog):
generate_profit_graph=profit_mock,
store_plot_file=store_mock
)
with pytest.raises(OperationalException,
match=r"No trades found, cannot generate Profit-plot.*"):
plot_profit(default_conf)
default_conf['exportfilename'] = testdatadir / "backtest-result_test.json"
plot_profit(default_conf)
# Plot-profit generates one combined plot

Binary file not shown.

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

Binary file not shown.

File diff suppressed because one or more lines are too long