Merge branch 'develop' into pr/yazeed/3091
This commit is contained in:
@@ -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):
|
||||
|
@@ -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)
|
||||
@@ -939,8 +940,7 @@ def test_hyperopt_list(mocker, capsys, hyperopt_results):
|
||||
pargs['config'] = None
|
||||
start_hyperopt_list(pargs)
|
||||
captured = capsys.readouterr()
|
||||
assert all(x in captured.out
|
||||
for x in ["CSV-File created!"])
|
||||
log_has("CSV file created: test_file.csv", caplog)
|
||||
f = Path("test_file.csv")
|
||||
assert 'Best,1,2,-1.25%,-0.00125625,,-2.51,"3,930.0 m",0.43662' in f.read_text()
|
||||
assert f.is_file()
|
||||
@@ -1069,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)
|
||||
|
@@ -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,
|
||||
|
@@ -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,10 +163,70 @@ 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
|
||||
|
||||
|
||||
def create_mock_trades(fee):
|
||||
"""
|
||||
Create some fake trades ...
|
||||
"""
|
||||
# Simulate dry_run entries
|
||||
trade = Trade(
|
||||
pair='ETH/BTC',
|
||||
stake_amount=0.001,
|
||||
amount=123.0,
|
||||
fee_open=fee.return_value,
|
||||
fee_close=fee.return_value,
|
||||
open_rate=0.123,
|
||||
exchange='bittrex',
|
||||
open_order_id='dry_run_buy_12345'
|
||||
)
|
||||
Trade.session.add(trade)
|
||||
|
||||
trade = Trade(
|
||||
pair='ETC/BTC',
|
||||
stake_amount=0.001,
|
||||
amount=123.0,
|
||||
fee_open=fee.return_value,
|
||||
fee_close=fee.return_value,
|
||||
open_rate=0.123,
|
||||
close_rate=0.128,
|
||||
close_profit=0.005,
|
||||
exchange='bittrex',
|
||||
is_open=False,
|
||||
open_order_id='dry_run_sell_12345'
|
||||
)
|
||||
Trade.session.add(trade)
|
||||
|
||||
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,
|
||||
fee_open=fee.return_value,
|
||||
fee_close=fee.return_value,
|
||||
open_rate=0.123,
|
||||
exchange='bittrex',
|
||||
open_order_id='prod_buy_12345'
|
||||
)
|
||||
Trade.session.add(trade)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def patch_coingekko(mocker) -> None:
|
||||
"""
|
||||
@@ -201,8 +262,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,
|
||||
@@ -258,7 +320,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
|
||||
|
||||
@@ -612,7 +675,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',
|
||||
@@ -693,6 +757,56 @@ def shitcoinmarkets(markets):
|
||||
"future": False,
|
||||
"active": True
|
||||
},
|
||||
'ADAHALF/USDT': {
|
||||
"percentage": True,
|
||||
"tierBased": False,
|
||||
"taker": 0.001,
|
||||
"maker": 0.001,
|
||||
"precision": {
|
||||
"base": 8,
|
||||
"quote": 8,
|
||||
"amount": 2,
|
||||
"price": 4
|
||||
},
|
||||
"limits": {
|
||||
},
|
||||
"id": "ADAHALFUSDT",
|
||||
"symbol": "ADAHALF/USDT",
|
||||
"base": "ADAHALF",
|
||||
"quote": "USDT",
|
||||
"baseId": "ADAHALF",
|
||||
"quoteId": "USDT",
|
||||
"info": {},
|
||||
"type": "spot",
|
||||
"spot": True,
|
||||
"future": False,
|
||||
"active": True
|
||||
},
|
||||
'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
|
||||
|
||||
@@ -708,10 +822,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'
|
||||
}
|
||||
@@ -723,10 +839,11 @@ def market_buy_order():
|
||||
'id': 'mocked_market_buy',
|
||||
'type': 'market',
|
||||
'side': 'buy',
|
||||
'pair': 'mocked',
|
||||
'symbol': 'mocked',
|
||||
'datetime': arrow.utcnow().isoformat(),
|
||||
'price': 0.00004099,
|
||||
'amount': 91.99181073,
|
||||
'filled': 91.99181073,
|
||||
'remaining': 0.0,
|
||||
'status': 'closed'
|
||||
}
|
||||
@@ -738,10 +855,11 @@ def market_sell_order():
|
||||
'id': 'mocked_limit_sell',
|
||||
'type': 'market',
|
||||
'side': 'sell',
|
||||
'pair': 'mocked',
|
||||
'symbol': 'mocked',
|
||||
'datetime': arrow.utcnow().isoformat(),
|
||||
'price': 0.00004173,
|
||||
'amount': 91.99181073,
|
||||
'filled': 91.99181073,
|
||||
'remaining': 0.0,
|
||||
'status': 'closed'
|
||||
}
|
||||
@@ -753,10 +871,11 @@ def limit_buy_order_old():
|
||||
'id': 'mocked_limit_buy_old',
|
||||
'type': 'limit',
|
||||
'side': 'buy',
|
||||
'pair': 'mocked',
|
||||
'symbol': 'mocked',
|
||||
'datetime': str(arrow.utcnow().shift(minutes=-601).datetime),
|
||||
'price': 0.00001099,
|
||||
'amount': 90.99181073,
|
||||
'filled': 0.0,
|
||||
'remaining': 90.99181073,
|
||||
'status': 'open'
|
||||
}
|
||||
@@ -768,10 +887,11 @@ def limit_sell_order_old():
|
||||
'id': 'mocked_limit_sell_old',
|
||||
'type': 'limit',
|
||||
'side': 'sell',
|
||||
'pair': 'ETH/BTC',
|
||||
'symbol': 'ETH/BTC',
|
||||
'datetime': arrow.utcnow().shift(minutes=-601).isoformat(),
|
||||
'price': 0.00001099,
|
||||
'amount': 90.99181073,
|
||||
'filled': 0.0,
|
||||
'remaining': 90.99181073,
|
||||
'status': 'open'
|
||||
}
|
||||
@@ -783,10 +903,11 @@ def limit_buy_order_old_partial():
|
||||
'id': 'mocked_limit_buy_old_partial',
|
||||
'type': 'limit',
|
||||
'side': 'buy',
|
||||
'pair': 'ETH/BTC',
|
||||
'symbol': 'ETH/BTC',
|
||||
'datetime': arrow.utcnow().shift(minutes=-601).isoformat(),
|
||||
'price': 0.00001099,
|
||||
'amount': 90.99181073,
|
||||
'filled': 23.0,
|
||||
'remaining': 67.99181073,
|
||||
'status': 'open'
|
||||
}
|
||||
@@ -796,10 +917,103 @@ def limit_buy_order_old_partial():
|
||||
def limit_buy_order_old_partial_canceled(limit_buy_order_old_partial):
|
||||
res = deepcopy(limit_buy_order_old_partial)
|
||||
res['status'] = 'canceled'
|
||||
res['fee'] = {'cost': 0.0001, 'currency': 'ETH'}
|
||||
res['fee'] = {'cost': 0.023, 'currency': 'ETH'}
|
||||
return res
|
||||
|
||||
|
||||
@pytest.fixture(scope='function')
|
||||
def limit_buy_order_canceled_empty(request):
|
||||
# Indirect fixture
|
||||
# Documentation:
|
||||
# https://docs.pytest.org/en/latest/example/parametrize.html#apply-indirect-on-particular-arguments
|
||||
|
||||
exchange_name = request.param
|
||||
if exchange_name == 'ftx':
|
||||
return {
|
||||
'info': {},
|
||||
'id': '1234512345',
|
||||
'clientOrderId': None,
|
||||
'timestamp': arrow.utcnow().shift(minutes=-601).timestamp,
|
||||
'datetime': arrow.utcnow().shift(minutes=-601).isoformat(),
|
||||
'lastTradeTimestamp': None,
|
||||
'symbol': 'LTC/USDT',
|
||||
'type': 'limit',
|
||||
'side': 'buy',
|
||||
'price': 34.3225,
|
||||
'amount': 0.55,
|
||||
'cost': 0.0,
|
||||
'average': None,
|
||||
'filled': 0.0,
|
||||
'remaining': 0.0,
|
||||
'status': 'closed',
|
||||
'fee': None,
|
||||
'trades': None
|
||||
}
|
||||
elif exchange_name == 'kraken':
|
||||
return {
|
||||
'info': {},
|
||||
'id': 'AZNPFF-4AC4N-7MKTAT',
|
||||
'clientOrderId': None,
|
||||
'timestamp': arrow.utcnow().shift(minutes=-601).timestamp,
|
||||
'datetime': arrow.utcnow().shift(minutes=-601).isoformat(),
|
||||
'lastTradeTimestamp': None,
|
||||
'status': 'canceled',
|
||||
'symbol': 'LTC/USDT',
|
||||
'type': 'limit',
|
||||
'side': 'buy',
|
||||
'price': 34.3225,
|
||||
'cost': 0.0,
|
||||
'amount': 0.55,
|
||||
'filled': 0.0,
|
||||
'average': 0.0,
|
||||
'remaining': 0.55,
|
||||
'fee': {'cost': 0.0, 'rate': None, 'currency': 'USDT'},
|
||||
'trades': []
|
||||
}
|
||||
elif exchange_name == 'binance':
|
||||
return {
|
||||
'info': {},
|
||||
'id': '1234512345',
|
||||
'clientOrderId': 'alb1234123',
|
||||
'timestamp': arrow.utcnow().shift(minutes=-601).timestamp,
|
||||
'datetime': arrow.utcnow().shift(minutes=-601).isoformat(),
|
||||
'lastTradeTimestamp': None,
|
||||
'symbol': 'LTC/USDT',
|
||||
'type': 'limit',
|
||||
'side': 'buy',
|
||||
'price': 0.016804,
|
||||
'amount': 0.55,
|
||||
'cost': 0.0,
|
||||
'average': None,
|
||||
'filled': 0.0,
|
||||
'remaining': 0.55,
|
||||
'status': 'canceled',
|
||||
'fee': None,
|
||||
'trades': None
|
||||
}
|
||||
else:
|
||||
return {
|
||||
'info': {},
|
||||
'id': '1234512345',
|
||||
'clientOrderId': 'alb1234123',
|
||||
'timestamp': arrow.utcnow().shift(minutes=-601).timestamp,
|
||||
'datetime': arrow.utcnow().shift(minutes=-601).isoformat(),
|
||||
'lastTradeTimestamp': None,
|
||||
'symbol': 'LTC/USDT',
|
||||
'type': 'limit',
|
||||
'side': 'buy',
|
||||
'price': 0.016804,
|
||||
'amount': 0.55,
|
||||
'cost': 0.0,
|
||||
'average': None,
|
||||
'filled': 0.0,
|
||||
'remaining': 0.55,
|
||||
'status': 'canceled',
|
||||
'fee': None,
|
||||
'trades': None
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def limit_sell_order():
|
||||
return {
|
||||
@@ -810,6 +1024,7 @@ def limit_sell_order():
|
||||
'datetime': arrow.utcnow().isoformat(),
|
||||
'price': 0.00001173,
|
||||
'amount': 90.99181073,
|
||||
'filled': 90.99181073,
|
||||
'remaining': 0.0,
|
||||
'status': 'closed'
|
||||
}
|
||||
@@ -1190,6 +1405,51 @@ def tickers():
|
||||
"quoteVolume": 323652.075405,
|
||||
"info": {}
|
||||
},
|
||||
# Example of leveraged pair with incomplete info
|
||||
"ADAHALF/USDT": {
|
||||
"symbol": "ADAHALF/USDT",
|
||||
"timestamp": 1580469388244,
|
||||
"datetime": "2020-01-31T11:16:28.244Z",
|
||||
"high": None,
|
||||
"low": None,
|
||||
"bid": 0.7305,
|
||||
"bidVolume": None,
|
||||
"ask": 0.7342,
|
||||
"askVolume": None,
|
||||
"vwap": None,
|
||||
"open": None,
|
||||
"close": None,
|
||||
"last": None,
|
||||
"previousClose": None,
|
||||
"change": None,
|
||||
"percentage": 2.628,
|
||||
"average": None,
|
||||
"baseVolume": 0.0,
|
||||
"quoteVolume": 0.0,
|
||||
"info": {}
|
||||
},
|
||||
"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": {}
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -1227,6 +1487,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',
|
||||
@@ -1381,10 +1650,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
|
||||
@@ -1500,7 +1770,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
|
||||
@@ -1511,11 +1781,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,
|
||||
@@ -1562,8 +1833,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,
|
||||
|
@@ -15,7 +15,7 @@ from freqtrade.data.btanalysis import (BT_DATA_COLUMNS,
|
||||
load_backtest_data, load_trades,
|
||||
load_trades_from_db)
|
||||
from freqtrade.data.history import load_data, load_pair_history
|
||||
from tests.test_persistence import create_mock_trades
|
||||
from tests.conftest import create_mock_trades
|
||||
|
||||
|
||||
def test_load_backtest_data(testdatadir):
|
||||
@@ -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"
|
||||
@@ -191,3 +195,28 @@ def test_calculate_max_drawdown(testdatadir):
|
||||
assert low == Timestamp('2018-01-30 04:45:00', tz='UTC')
|
||||
with pytest.raises(ValueError, match='Trade dataframe empty.'):
|
||||
drawdown, h, low = calculate_max_drawdown(DataFrame())
|
||||
|
||||
|
||||
def test_calculate_max_drawdown2():
|
||||
values = [0.011580, 0.010048, 0.011340, 0.012161, 0.010416, 0.010009, 0.020024,
|
||||
-0.024662, -0.022350, 0.020496, -0.029859, -0.030511, 0.010041, 0.010872,
|
||||
-0.025782, 0.010400, 0.012374, 0.012467, 0.114741, 0.010303, 0.010088,
|
||||
-0.033961, 0.010680, 0.010886, -0.029274, 0.011178, 0.010693, 0.010711]
|
||||
|
||||
dates = [Arrow(2020, 1, 1).shift(days=i) for i in range(len(values))]
|
||||
df = DataFrame(zip(values, dates), columns=['profit', 'open_time'])
|
||||
# sort by profit and reset index
|
||||
df = df.sort_values('profit').reset_index(drop=True)
|
||||
df1 = df.copy()
|
||||
drawdown, h, low = calculate_max_drawdown(df, date_col='open_time', value_col='profit')
|
||||
# Ensure df has not been altered.
|
||||
assert df.equals(df1)
|
||||
|
||||
assert isinstance(drawdown, float)
|
||||
# High must be before low
|
||||
assert h < low
|
||||
assert drawdown == 0.091755
|
||||
|
||||
df = DataFrame(zip(values[:5], dates[:5]), columns=['profit', 'open_time'])
|
||||
with pytest.raises(ValueError, match='No losing trade, therefore no drawdown.'):
|
||||
calculate_max_drawdown(df, date_col='open_time', value_col='profit')
|
||||
|
@@ -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):
|
||||
|
@@ -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)
|
||||
|
@@ -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())
|
||||
|
@@ -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
|
||||
|
@@ -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):
|
||||
|
@@ -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):
|
||||
@@ -253,6 +274,32 @@ def test_price_to_precision(default_conf, mocker, price, precision_mode, precisi
|
||||
assert pytest.approx(exchange.price_to_precision(pair, price)) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize("price,precision_mode,precision,expected", [
|
||||
(2.34559, 2, 4, 0.0001),
|
||||
(2.34559, 2, 5, 0.00001),
|
||||
(2.34559, 2, 3, 0.001),
|
||||
(2.9999, 2, 3, 0.001),
|
||||
(200.0511, 2, 3, 0.001),
|
||||
# Tests for Tick_size
|
||||
(2.34559, 4, 0.0001, 0.0001),
|
||||
(2.34559, 4, 0.00001, 0.00001),
|
||||
(2.34559, 4, 0.0025, 0.0025),
|
||||
(2.9909, 4, 0.0025, 0.0025),
|
||||
(234.43, 4, 0.5, 0.5),
|
||||
(234.43, 4, 0.0025, 0.0025),
|
||||
(234.43, 4, 0.00013, 0.00013),
|
||||
|
||||
])
|
||||
def test_price_get_one_pip(default_conf, mocker, price, precision_mode, precision, expected):
|
||||
markets = PropertyMock(return_value={'ETH/BTC': {'precision': {'price': precision}}})
|
||||
exchange = get_patched_exchange(mocker, default_conf, id="binance")
|
||||
mocker.patch('freqtrade.exchange.Exchange.markets', markets)
|
||||
mocker.patch('freqtrade.exchange.Exchange.precisionMode',
|
||||
PropertyMock(return_value=precision_mode))
|
||||
pair = 'ETH/BTC'
|
||||
assert pytest.approx(exchange.price_get_one_pip(pair, price)) == expected
|
||||
|
||||
|
||||
def test_set_sandbox(default_conf, mocker):
|
||||
"""
|
||||
Test working scenario
|
||||
@@ -289,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
|
||||
@@ -322,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': {}}
|
||||
|
||||
@@ -335,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()
|
||||
@@ -360,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)
|
||||
|
||||
@@ -491,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):
|
||||
@@ -548,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
|
||||
@@ -566,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
|
||||
@@ -583,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."):
|
||||
@@ -591,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
|
||||
@@ -611,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
|
||||
@@ -632,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
|
||||
@@ -659,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})
|
||||
@@ -675,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)
|
||||
@@ -688,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)
|
||||
@@ -1089,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() == {}
|
||||
|
||||
|
||||
@@ -1228,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
|
||||
@@ -1320,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,
|
||||
@@ -1387,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
|
||||
@@ -1401,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):
|
||||
@@ -1450,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]
|
||||
|
||||
@@ -1487,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
|
||||
@@ -1511,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
|
||||
@@ -1568,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:
|
||||
@@ -1578,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
|
||||
@@ -1588,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
|
||||
@@ -1604,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:]
|
||||
@@ -1614,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
|
||||
@@ -1624,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)
|
||||
|
||||
|
||||
@@ -1640,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 []
|
||||
@@ -1650,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
|
||||
@@ -1660,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)
|
||||
@@ -1672,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,
|
||||
@@ -1694,15 +1749,78 @@ def test_get_historic_trades_notsupported(default_conf, mocker, caplog, exchange
|
||||
|
||||
with pytest.raises(OperationalException,
|
||||
match="This exchange does not suport downloading Trades."):
|
||||
exchange.get_historic_trades(pair, since=trades_history[0]["timestamp"],
|
||||
until=trades_history[-1]["timestamp"])
|
||||
exchange.get_historic_trades(pair, since=trades_history[0][0],
|
||||
until=trades_history[-1][0])
|
||||
|
||||
|
||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||
def test_cancel_order_dry_run(default_conf, mocker, exchange_name):
|
||||
default_conf['dry_run'] = True
|
||||
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
|
||||
assert exchange.cancel_order(order_id='123', pair='TKN/BTC') is None
|
||||
assert exchange.cancel_order(order_id='123', pair='TKN/BTC') == {}
|
||||
assert exchange.cancel_stoploss_order(order_id='123', pair='TKN/BTC') == {}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||
@pytest.mark.parametrize("order,result", [
|
||||
({'status': 'closed', 'filled': 10}, False),
|
||||
({'status': 'closed', 'filled': 0.0}, True),
|
||||
({'status': 'canceled', 'filled': 0.0}, True),
|
||||
({'status': 'canceled', 'filled': 10.0}, False),
|
||||
({'status': 'unknown', 'filled': 10.0}, False),
|
||||
({'result': 'testest123'}, False),
|
||||
])
|
||||
def test_check_order_canceled_empty(mocker, default_conf, exchange_name, order, result):
|
||||
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
|
||||
assert exchange.check_order_canceled_empty(order) == result
|
||||
|
||||
|
||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||
@pytest.mark.parametrize("order,result", [
|
||||
({'status': 'closed', 'amount': 10, 'fee': {}}, True),
|
||||
({'status': 'closed', 'amount': 0.0, 'fee': {}}, True),
|
||||
({'status': 'canceled', 'amount': 0.0, 'fee': {}}, True),
|
||||
({'status': 'canceled', 'amount': 10.0}, False),
|
||||
({'amount': 10.0, 'fee': {}}, False),
|
||||
({'result': 'testest123'}, False),
|
||||
('hello_world', False),
|
||||
])
|
||||
def test_is_cancel_order_result_suitable(mocker, default_conf, exchange_name, order, result):
|
||||
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
|
||||
assert exchange.is_cancel_order_result_suitable(order) == result
|
||||
|
||||
|
||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||
@pytest.mark.parametrize("corder,call_corder,call_forder", [
|
||||
({'status': 'closed', 'amount': 10, 'fee': {}}, 1, 0),
|
||||
({'amount': 10, 'fee': {}}, 1, 1),
|
||||
])
|
||||
def test_cancel_order_with_result(default_conf, mocker, exchange_name, corder,
|
||||
call_corder, call_forder):
|
||||
default_conf['dry_run'] = False
|
||||
api_mock = MagicMock()
|
||||
api_mock.cancel_order = MagicMock(return_value=corder)
|
||||
api_mock.fetch_order = MagicMock(return_value={})
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||
res = exchange.cancel_order_with_result('1234', 'ETH/BTC', 1234)
|
||||
assert isinstance(res, dict)
|
||||
assert api_mock.cancel_order.call_count == call_corder
|
||||
assert api_mock.fetch_order.call_count == call_forder
|
||||
|
||||
|
||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||
def test_cancel_order_with_result_error(default_conf, mocker, exchange_name, caplog):
|
||||
default_conf['dry_run'] = False
|
||||
api_mock = MagicMock()
|
||||
api_mock.cancel_order = MagicMock(side_effect=ccxt.InvalidOrder("Did not find order"))
|
||||
api_mock.fetch_order = MagicMock(side_effect=ccxt.InvalidOrder("Did not find order"))
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||
|
||||
res = exchange.cancel_order_with_result('1234', 'ETH/BTC', 1541)
|
||||
assert isinstance(res, dict)
|
||||
assert log_has("Could not cancel order 1234 for ETH/BTC.", caplog)
|
||||
assert log_has("Could not fetch cancelled order 1234.", caplog)
|
||||
assert res['amount'] == 1541
|
||||
|
||||
|
||||
# Ensure that if not dry_run, we should call API
|
||||
@@ -1726,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')
|
||||
|
||||
|
||||
@@ -1958,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
|
||||
@@ -2057,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
158
tests/exchange/test_ftx.py
Normal 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')
|
@@ -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)
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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,
|
||||
@@ -817,7 +821,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:
|
||||
|
@@ -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):
|
||||
@@ -160,10 +231,14 @@ def test_backtest_record(default_conf, fee, mocker):
|
||||
# reset test to test with strategy name
|
||||
names = []
|
||||
records = []
|
||||
results['Strat'] = pd.DataFrame()
|
||||
results['Strat'] = results['DefStrat']
|
||||
results['Strat2'] = results['DefStrat']
|
||||
store_backtest_result(Path("backtest-result.json"), results)
|
||||
# Assert file_dump_json was only called once
|
||||
assert names == [Path('backtest-result-DefStrat.json')]
|
||||
assert names == [
|
||||
Path('backtest-result-DefStrat.json'),
|
||||
Path('backtest-result-Strat.json'),
|
||||
Path('backtest-result-Strat2.json'),
|
||||
]
|
||||
records = records[0]
|
||||
# Ensure records are of correct type
|
||||
assert len(records) == 4
|
||||
|
@@ -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'] = [
|
||||
@@ -46,45 +92,67 @@ def static_pl_conf(whitelist_conf):
|
||||
return whitelist_conf
|
||||
|
||||
|
||||
def test_log_on_refresh(mocker, static_pl_conf, markets, tickers):
|
||||
mocker.patch.multiple('freqtrade.exchange.Exchange',
|
||||
markets=PropertyMock(return_value=markets),
|
||||
exchange_has=MagicMock(return_value=True),
|
||||
get_tickers=tickers
|
||||
)
|
||||
freqtrade = get_patched_freqtradebot(mocker, static_pl_conf)
|
||||
logmock = MagicMock()
|
||||
# Assign starting whitelist
|
||||
pl = freqtrade.pairlists._pairlist_handlers[0]
|
||||
pl.log_on_refresh(logmock, 'Hello world')
|
||||
assert logmock.call_count == 1
|
||||
pl.log_on_refresh(logmock, 'Hello world')
|
||||
assert logmock.call_count == 1
|
||||
assert pl._log_cache.currsize == 1
|
||||
assert ('Hello world',) in pl._log_cache._Cache__data
|
||||
|
||||
pl.log_on_refresh(logmock, 'Hello world2')
|
||||
assert logmock.call_count == 2
|
||||
assert pl._log_cache.currsize == 2
|
||||
|
||||
|
||||
def test_load_pairlist_noexist(mocker, markets, default_conf):
|
||||
bot = get_patched_freqtradebot(mocker, default_conf)
|
||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||
mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets))
|
||||
plm = PairListManager(bot.exchange, default_conf)
|
||||
plm = PairListManager(freqtrade.exchange, default_conf)
|
||||
with pytest.raises(OperationalException,
|
||||
match=r"Impossible to load Pairlist 'NonexistingPairList'. "
|
||||
r"This class does not exist or contains Python code errors."):
|
||||
PairListResolver.load_pairlist('NonexistingPairList', bot.exchange, plm,
|
||||
PairListResolver.load_pairlist('NonexistingPairList', freqtrade.exchange, plm,
|
||||
default_conf, {}, 1)
|
||||
|
||||
|
||||
def test_refresh_market_pair_not_in_whitelist(mocker, markets, static_pl_conf):
|
||||
|
||||
freqtradebot = get_patched_freqtradebot(mocker, static_pl_conf)
|
||||
freqtrade = get_patched_freqtradebot(mocker, static_pl_conf)
|
||||
|
||||
mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets))
|
||||
freqtradebot.pairlists.refresh_pairlist()
|
||||
freqtrade.pairlists.refresh_pairlist()
|
||||
# List ordered by BaseVolume
|
||||
whitelist = ['ETH/BTC', 'TKN/BTC']
|
||||
# Ensure all except those in whitelist are removed
|
||||
assert set(whitelist) == set(freqtradebot.pairlists.whitelist)
|
||||
assert set(whitelist) == set(freqtrade.pairlists.whitelist)
|
||||
# Ensure config dict hasn't been changed
|
||||
assert (static_pl_conf['exchange']['pair_whitelist'] ==
|
||||
freqtradebot.config['exchange']['pair_whitelist'])
|
||||
freqtrade.config['exchange']['pair_whitelist'])
|
||||
|
||||
|
||||
def test_refresh_static_pairlist(mocker, markets, static_pl_conf):
|
||||
freqtradebot = get_patched_freqtradebot(mocker, static_pl_conf)
|
||||
freqtrade = get_patched_freqtradebot(mocker, static_pl_conf)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
exchange_has=MagicMock(return_value=True),
|
||||
markets=PropertyMock(return_value=markets),
|
||||
)
|
||||
freqtradebot.pairlists.refresh_pairlist()
|
||||
freqtrade.pairlists.refresh_pairlist()
|
||||
# List ordered by BaseVolume
|
||||
whitelist = ['ETH/BTC', 'TKN/BTC']
|
||||
# Ensure all except those in whitelist are removed
|
||||
assert set(whitelist) == set(freqtradebot.pairlists.whitelist)
|
||||
assert static_pl_conf['exchange']['pair_blacklist'] == freqtradebot.pairlists.blacklist
|
||||
assert set(whitelist) == set(freqtrade.pairlists.whitelist)
|
||||
assert static_pl_conf['exchange']['pair_blacklist'] == freqtrade.pairlists.blacklist
|
||||
|
||||
|
||||
def test_refresh_pairlist_dynamic(mocker, shitcoinmarkets, tickers, whitelist_conf):
|
||||
@@ -94,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):
|
||||
@@ -122,97 +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']),
|
||||
# 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']),
|
||||
# Hot is removed by precision_filter, Fuel by low_price_filter.
|
||||
"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_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)
|
||||
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)
|
||||
|
||||
|
||||
@@ -255,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"})
|
||||
|
||||
@@ -277,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):
|
||||
@@ -297,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)
|
||||
|
@@ -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
|
||||
from tests.conftest import (create_mock_trades, get_patched_freqtradebot,
|
||||
patch_get_signal)
|
||||
|
||||
|
||||
# Functions for recurrent object patching
|
||||
@@ -42,55 +43,131 @@ 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_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': 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,
|
||||
'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_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,
|
||||
'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:
|
||||
@@ -133,7 +210,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]
|
||||
@@ -171,22 +248,104 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee,
|
||||
# Try valid data
|
||||
update.message.text = '/daily 2'
|
||||
days = rpc._rpc_daily_profit(7, stake_currency, fiat_display_currency)
|
||||
assert len(days) == 7
|
||||
for day in days:
|
||||
assert len(days['data']) == 7
|
||||
assert days['stake_currency'] == default_conf['stake_currency']
|
||||
assert days['fiat_display_currency'] == default_conf['fiat_display_currency']
|
||||
for day in days['data']:
|
||||
# [datetime.date(2018, 1, 11), '0.00000000 BTC', '0.000 USD']
|
||||
assert (day[1] == '0.00000000 BTC' or
|
||||
day[1] == '0.00006217 BTC')
|
||||
assert (day['abs_profit'] == '0.00000000' or
|
||||
day['abs_profit'] == '0.00006217')
|
||||
|
||||
assert (day[2] == '0.000 USD' or
|
||||
day[2] == '0.767 USD')
|
||||
assert (day['fiat_value'] == '0.000' or
|
||||
day['fiat_value'] == '0.767')
|
||||
# ensure first day is current date
|
||||
assert str(days[0][0]) == str(datetime.utcnow().date())
|
||||
assert str(days['data'][0]['date']) == str(datetime.utcnow().date())
|
||||
|
||||
# Try invalid data
|
||||
with pytest.raises(RPCException, match=r'.*must be an integer greater than 0*'):
|
||||
rpc._rpc_daily_profit(0, stake_currency, fiat_display_currency)
|
||||
|
||||
|
||||
def test_rpc_trade_history(mocker, default_conf, markets, fee):
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
markets=PropertyMock(return_value=markets)
|
||||
)
|
||||
|
||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||
create_mock_trades(fee)
|
||||
rpc = RPC(freqtradebot)
|
||||
rpc._fiat_converter = CryptoToFiatConverter()
|
||||
trades = rpc._rpc_trade_history(2)
|
||||
assert len(trades['trades']) == 2
|
||||
assert trades['trades_count'] == 2
|
||||
assert isinstance(trades['trades'][0], dict)
|
||||
assert isinstance(trades['trades'][1], dict)
|
||||
|
||||
trades = rpc._rpc_trade_history(0)
|
||||
assert len(trades['trades']) == 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,
|
||||
limit_buy_order, limit_sell_order, mocker) -> None:
|
||||
mocker.patch.multiple(
|
||||
@@ -209,8 +368,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()
|
||||
@@ -257,7 +420,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'
|
||||
@@ -486,7 +649,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
|
||||
|
||||
|
||||
@@ -498,7 +661,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',
|
||||
@@ -544,7 +707,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',
|
||||
@@ -563,7 +726,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',
|
||||
@@ -580,7 +743,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',
|
||||
@@ -763,6 +926,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())
|
||||
|
@@ -13,7 +13,7 @@ from freqtrade.__init__ import __version__
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.rpc.api_server import BASE_URI, ApiServer
|
||||
from freqtrade.state import State
|
||||
from tests.conftest import get_patched_freqtradebot, log_has, patch_get_signal
|
||||
from tests.conftest import get_patched_freqtradebot, log_has, patch_get_signal, create_mock_trades
|
||||
|
||||
_TEST_USER = "FreqTrader"
|
||||
_TEST_PASS = "SuperSecurePassword1!"
|
||||
@@ -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,8 +352,75 @@ 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, fee, markets):
|
||||
ftbot, client = botclient
|
||||
patch_get_signal(ftbot, (True, False))
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
markets=PropertyMock(return_value=markets)
|
||||
)
|
||||
rc = client_get(client, f"{BASE_URI}/trades")
|
||||
assert_response(rc)
|
||||
assert len(rc.json) == 2
|
||||
assert rc.json['trades_count'] == 0
|
||||
|
||||
create_mock_trades(fee)
|
||||
|
||||
rc = client_get(client, f"{BASE_URI}/trades")
|
||||
assert_response(rc)
|
||||
assert len(rc.json['trades']) == 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):
|
||||
@@ -329,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()
|
||||
@@ -339,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)
|
||||
|
||||
@@ -353,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,
|
||||
}
|
||||
|
||||
|
||||
@@ -423,6 +559,10 @@ 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
|
||||
@@ -430,21 +570,60 @@ def test_api_status(botclient, mocker, ticker, fee, markets):
|
||||
'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,
|
||||
'trade_id': 1}]
|
||||
'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': 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,
|
||||
'timeframe': 5,
|
||||
'exchange': 'bittrex',
|
||||
}]
|
||||
|
||||
|
||||
def test_api_version(botclient):
|
||||
@@ -462,7 +641,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",
|
||||
@@ -470,7 +651,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):
|
||||
@@ -521,19 +704,50 @@ def test_api_forcebuy(botclient, mocker, fee):
|
||||
data='{"pair": "ETH/BTC"}')
|
||||
assert_response(rc)
|
||||
assert rc.json == {'amount': 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,
|
||||
'sell_reason': None,
|
||||
'sell_order_status': None,
|
||||
'strategy': None,
|
||||
'ticker_interval': None,
|
||||
'timeframe': None,
|
||||
'exchange': 'bittrex',
|
||||
}
|
||||
|
||||
|
||||
def test_api_forcesell(botclient, mocker, ticker, fee, markets):
|
||||
|
@@ -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,8 +685,8 @@ 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]
|
||||
|
||||
@@ -717,6 +726,7 @@ 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',
|
||||
@@ -776,6 +786,7 @@ 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',
|
||||
@@ -824,6 +835,7 @@ 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',
|
||||
@@ -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'
|
||||
@@ -1316,18 +1396,21 @@ def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None:
|
||||
'type': RPCMessageType.SELL_CANCEL_NOTIFICATION,
|
||||
'exchange': 'Binance',
|
||||
'pair': 'KEY/ETH',
|
||||
'reason': 'Cancelled on exchange'
|
||||
})
|
||||
assert msg_mock.call_args[0][0] \
|
||||
== ('*Binance:* Cancelling Open Sell Order for KEY/ETH')
|
||||
== ('\N{WARNING SIGN} *Binance:* Cancelling Open Sell Order for KEY/ETH. '
|
||||
'Reason: Cancelled on exchange')
|
||||
|
||||
msg_mock.reset_mock()
|
||||
telegram.send_msg({
|
||||
'type': RPCMessageType.SELL_CANCEL_NOTIFICATION,
|
||||
'exchange': 'Binance',
|
||||
'pair': 'KEY/ETH',
|
||||
'reason': 'timeout'
|
||||
})
|
||||
assert msg_mock.call_args[0][0] \
|
||||
== ('*Binance:* Cancelling Open Sell Order for KEY/ETH')
|
||||
== ('\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
|
||||
|
||||
@@ -1361,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:
|
||||
@@ -1419,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:
|
||||
@@ -1455,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:
|
||||
|
@@ -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 = {
|
||||
|
@@ -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:
|
||||
|
@@ -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
|
||||
|
@@ -4,103 +4,153 @@ import logging
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import arrow
|
||||
import pytest
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.configuration import TimeRange
|
||||
from freqtrade.data.history import load_data
|
||||
from freqtrade.exceptions import StrategyError
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.resolvers import StrategyResolver
|
||||
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
|
||||
from freqtrade.data.dataprovider import DataProvider
|
||||
from tests.conftest import log_has, log_has_re
|
||||
|
||||
from .strats.default_strategy import DefaultStrategy
|
||||
from tests.conftest import get_patched_exchange, log_has
|
||||
|
||||
# Avoid to reinit the same object again and again
|
||||
_STRATEGY = DefaultStrategy(config={})
|
||||
_STRATEGY.dp = DataProvider({}, None, None)
|
||||
|
||||
|
||||
def test_returns_latest_buy_signal(mocker, default_conf, ohlcv_history):
|
||||
mocker.patch.object(
|
||||
_STRATEGY, '_analyze_ticker_internal',
|
||||
return_value=DataFrame([{'buy': 1, 'sell': 0, 'date': arrow.utcnow()}])
|
||||
)
|
||||
assert _STRATEGY.get_signal('ETH/BTC', '5m', ohlcv_history) == (True, False)
|
||||
def test_returns_latest_signal(mocker, default_conf, ohlcv_history):
|
||||
ohlcv_history.loc[1, 'date'] = arrow.utcnow()
|
||||
# Take a copy to correctly modify the call
|
||||
mocked_history = ohlcv_history.copy()
|
||||
mocked_history['sell'] = 0
|
||||
mocked_history['buy'] = 0
|
||||
mocked_history.loc[1, 'sell'] = 1
|
||||
|
||||
mocker.patch.object(
|
||||
_STRATEGY, '_analyze_ticker_internal',
|
||||
return_value=DataFrame([{'buy': 0, 'sell': 1, 'date': arrow.utcnow()}])
|
||||
)
|
||||
assert _STRATEGY.get_signal('ETH/BTC', '5m', 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
|
||||
|
||||
assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_history) == (True, False)
|
||||
mocked_history.loc[1, 'sell'] = 0
|
||||
mocked_history.loc[1, 'buy'] = 0
|
||||
|
||||
assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_history) == (False, False)
|
||||
|
||||
|
||||
def test_returns_latest_sell_signal(mocker, default_conf, ohlcv_history):
|
||||
mocker.patch.object(
|
||||
_STRATEGY, '_analyze_ticker_internal',
|
||||
return_value=DataFrame([{'sell': 1, 'buy': 0, 'date': arrow.utcnow()}])
|
||||
)
|
||||
|
||||
assert _STRATEGY.get_signal('ETH/BTC', '5m', ohlcv_history) == (False, True)
|
||||
|
||||
mocker.patch.object(
|
||||
_STRATEGY, '_analyze_ticker_internal',
|
||||
return_value=DataFrame([{'sell': 0, 'buy': 1, 'date': arrow.utcnow()}])
|
||||
)
|
||||
assert _STRATEGY.get_signal('ETH/BTC', '5m', ohlcv_history) == (True, 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([])
|
||||
)
|
||||
assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['ticker_interval'],
|
||||
ohlcv_history)
|
||||
assert log_has('Empty dataframe for pair xyz', caplog)
|
||||
mocker.patch.object(_STRATEGY, 'assert_df')
|
||||
|
||||
_STRATEGY.analyze_pair('ETH/BTC')
|
||||
|
||||
assert log_has('Empty dataframe for pair ETH/BTC', caplog)
|
||||
|
||||
|
||||
def test_get_signal_old_dataframe(default_conf, mocker, caplog, ohlcv_history):
|
||||
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)
|
||||
# default_conf defines a 5m interval. we check interval * 2 + 5m
|
||||
# this is necessary as the last candle is removed (partial candles) by default
|
||||
oldtime = arrow.utcnow().shift(minutes=-16)
|
||||
ticks = DataFrame([{'buy': 1, 'date': oldtime}])
|
||||
mocker.patch.object(_STRATEGY.dp, 'ohlcv', return_value=ohlcv_history)
|
||||
mocker.patch.object(
|
||||
_STRATEGY, '_analyze_ticker_internal',
|
||||
return_value=DataFrame(ticks)
|
||||
side_effect=ValueError('xyz')
|
||||
)
|
||||
assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['ticker_interval'],
|
||||
ohlcv_history)
|
||||
assert log_has('Outdated history for pair xyz. Last tick is 16 minutes old', caplog)
|
||||
_STRATEGY.analyze_pair('foo')
|
||||
assert log_has_re(r'Strategy caused the following exception: xyz.*', caplog)
|
||||
caplog.clear()
|
||||
|
||||
|
||||
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.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):
|
||||
# default_conf defines a 5m interval. we check interval * 2 + 5m
|
||||
# this is necessary as the last candle is removed (partial candles) by default
|
||||
ohlcv_history.loc[1, 'date'] = arrow.utcnow().shift(minutes=-16)
|
||||
# Take a copy to correctly modify the call
|
||||
mocked_history = ohlcv_history.copy()
|
||||
mocked_history['sell'] = 0
|
||||
mocked_history['buy'] = 0
|
||||
mocked_history.loc[1, 'buy'] = 1
|
||||
|
||||
caplog.set_level(logging.INFO)
|
||||
mocker.patch.object(_STRATEGY, 'assert_df')
|
||||
|
||||
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)
|
||||
|
||||
|
||||
def test_assert_df_raise(default_conf, mocker, caplog, ohlcv_history):
|
||||
# default_conf defines a 5m interval. we check interval * 2 + 5m
|
||||
# this is necessary as the last candle is removed (partial candles) by default
|
||||
ohlcv_history.loc[1, 'date'] = arrow.utcnow().shift(minutes=-16)
|
||||
# Take a copy to correctly modify the call
|
||||
mocked_history = ohlcv_history.copy()
|
||||
mocked_history['sell'] = 0
|
||||
mocked_history['buy'] = 0
|
||||
mocked_history.loc[1, 'buy'] = 1
|
||||
|
||||
caplog.set_level(logging.INFO)
|
||||
mocker.patch.object(_STRATEGY.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=StrategyError('Dataframe returned...')
|
||||
)
|
||||
_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, 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(StrategyError, match=r"Dataframe returned from strategy.*length\."):
|
||||
_STRATEGY.assert_df(ohlcv_history, len(ohlcv_history) + 1,
|
||||
ohlcv_history.loc[1, 'close'], ohlcv_history.loc[1, 'date'])
|
||||
|
||||
with pytest.raises(StrategyError,
|
||||
match=r"Dataframe returned from strategy.*last close price\."):
|
||||
_STRATEGY.assert_df(ohlcv_history, len(ohlcv_history),
|
||||
ohlcv_history.loc[1, 'close'] + 0.01, ohlcv_history.loc[1, 'date'])
|
||||
with pytest.raises(StrategyError,
|
||||
match=r"Dataframe returned from strategy.*last date\."):
|
||||
_STRATEGY.assert_df(ohlcv_history, len(ohlcv_history),
|
||||
ohlcv_history.loc[1, 'close'], ohlcv_history.loc[0, 'date'])
|
||||
|
||||
_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:
|
||||
@@ -114,6 +164,19 @@ def test_ohlcvdata_to_dataframe(default_conf, testdatadir) -> None:
|
||||
assert len(processed['UNITTEST/BTC']) == 102 # partial candle was removed
|
||||
|
||||
|
||||
def test_ohlcvdata_to_dataframe_copy(mocker, default_conf, testdatadir) -> None:
|
||||
default_conf.update({'strategy': 'DefaultStrategy'})
|
||||
strategy = StrategyResolver.load_strategy(default_conf)
|
||||
aimock = mocker.patch('freqtrade.strategy.interface.IStrategy.advise_indicators')
|
||||
timerange = TimeRange.parse_timerange('1510694220-1510700340')
|
||||
data = load_data(testdatadir, '1m', ['UNITTEST/BTC'], timerange=timerange,
|
||||
fill_up_missing=True)
|
||||
strategy.ohlcvdata_to_dataframe(data)
|
||||
assert aimock.call_count == 1
|
||||
# Ensure that a copy of the dataframe is passed to advice_indicators
|
||||
assert aimock.call_args_list[0][0][0] is not data
|
||||
|
||||
|
||||
def test_min_roi_reached(default_conf, fee) -> None:
|
||||
|
||||
# Use list to confirm sequence does not matter
|
||||
@@ -266,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'})
|
||||
@@ -322,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
|
||||
|
@@ -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'})
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -18,7 +18,7 @@ from freqtrade.configuration.config_validation import validate_config_schema
|
||||
from freqtrade.configuration.deprecated_settings import (
|
||||
check_conflicting_settings, process_deprecated_setting,
|
||||
process_temporary_deprecated_settings)
|
||||
from freqtrade.configuration.load_config import load_config_file
|
||||
from freqtrade.configuration.load_config import load_config_file, log_config_error_range
|
||||
from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.loggers import _set_loggers, setup_logging
|
||||
@@ -66,6 +66,30 @@ def test_load_config_file(default_conf, mocker, caplog) -> None:
|
||||
assert validated_conf.items() >= default_conf.items()
|
||||
|
||||
|
||||
def test_load_config_file_error(default_conf, mocker, caplog) -> None:
|
||||
del default_conf['user_data_dir']
|
||||
filedata = json.dumps(default_conf).replace(
|
||||
'"stake_amount": 0.001,', '"stake_amount": .001,')
|
||||
mocker.patch('freqtrade.configuration.load_config.open', mocker.mock_open(read_data=filedata))
|
||||
mocker.patch.object(Path, "read_text", MagicMock(return_value=filedata))
|
||||
|
||||
with pytest.raises(OperationalException, match=r".*Please verify the following segment.*"):
|
||||
load_config_file('somefile')
|
||||
|
||||
|
||||
def test_load_config_file_error_range(default_conf, mocker, caplog) -> None:
|
||||
del default_conf['user_data_dir']
|
||||
filedata = json.dumps(default_conf).replace(
|
||||
'"stake_amount": 0.001,', '"stake_amount": .001,')
|
||||
mocker.patch.object(Path, "read_text", MagicMock(return_value=filedata))
|
||||
|
||||
x = log_config_error_range('somefile', 'Parse error at offset 64: Invalid value.')
|
||||
assert isinstance(x, str)
|
||||
assert (x == '{"max_open_trades": 1, "stake_currency": "BTC", '
|
||||
'"stake_amount": .001, "fiat_display_currency": "USD", '
|
||||
'"timeframe": "5m", "dry_run": true, "cance')
|
||||
|
||||
|
||||
def test__args_to_config(caplog):
|
||||
|
||||
arg_list = ['trade', '--strategy-path', 'TestTest']
|
||||
@@ -73,6 +97,7 @@ def test__args_to_config(caplog):
|
||||
configuration = Configuration(args)
|
||||
config = {}
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
warnings.simplefilter("always")
|
||||
# No warnings ...
|
||||
configuration._args_to_config(config, argname="strategy_path", logstring="DeadBeef")
|
||||
assert len(w) == 0
|
||||
@@ -82,6 +107,7 @@ def test__args_to_config(caplog):
|
||||
configuration = Configuration(args)
|
||||
config = {}
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
warnings.simplefilter("always")
|
||||
# Deprecation warnings!
|
||||
configuration._args_to_config(config, argname="strategy_path", logstring="DeadBeef",
|
||||
deprecated_msg="Going away soon!")
|
||||
@@ -375,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)
|
||||
@@ -422,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
|
||||
@@ -468,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
|
||||
@@ -628,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")
|
||||
@@ -843,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", ""),
|
||||
@@ -1015,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': {
|
||||
@@ -1034,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):
|
||||
@@ -1123,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)
|
||||
|
@@ -25,7 +25,7 @@ def test_create_userdata_dir(mocker, default_conf, caplog) -> None:
|
||||
md = mocker.patch.object(Path, 'mkdir', MagicMock())
|
||||
|
||||
x = create_userdata_dir('/tmp/bar', create_dir=True)
|
||||
assert md.call_count == 8
|
||||
assert md.call_count == 9
|
||||
assert md.call_args[1]['parents'] is False
|
||||
assert log_has(f'Created user-data directory: {Path("/tmp/bar")}', caplog)
|
||||
assert isinstance(x, Path)
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -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=[
|
||||
|
@@ -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)
|
||||
|
@@ -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, shorten_date)
|
||||
plural, render_template,
|
||||
render_template_with_fallback, safe_value_fallback,
|
||||
shorten_date)
|
||||
|
||||
|
||||
def test_shorten_date() -> None:
|
||||
@@ -93,6 +95,27 @@ def test_format_ms_time() -> None:
|
||||
assert format_ms_time(date_in_epoch_ms) == res.astimezone(None).strftime('%Y-%m-%dT%H:%M:%S')
|
||||
|
||||
|
||||
def test_safe_value_fallback():
|
||||
dict1 = {'keya': None, 'keyb': 2, 'keyc': 5, 'keyd': None}
|
||||
dict2 = {'keya': 20, 'keyb': None, 'keyc': 6, 'keyd': None}
|
||||
assert safe_value_fallback(dict1, dict2, 'keya', 'keya') == 20
|
||||
assert safe_value_fallback(dict2, dict1, 'keya', 'keya') == 20
|
||||
|
||||
assert safe_value_fallback(dict1, dict2, 'keyb', 'keyb') == 2
|
||||
assert safe_value_fallback(dict2, dict1, 'keyb', 'keyb') == 2
|
||||
|
||||
assert safe_value_fallback(dict1, dict2, 'keyc', 'keyc') == 5
|
||||
assert safe_value_fallback(dict2, dict1, 'keyc', 'keyc') == 6
|
||||
|
||||
assert safe_value_fallback(dict1, dict2, 'keyd', 'keyd') is None
|
||||
assert safe_value_fallback(dict2, dict1, 'keyd', 'keyd') is None
|
||||
assert safe_value_fallback(dict2, dict1, 'keyd', 'keyd', 1234) == 1234
|
||||
|
||||
assert safe_value_fallback(dict1, dict2, 'keyNo', 'keyNo') is None
|
||||
assert safe_value_fallback(dict2, dict1, 'keyNo', 'keyNo') is None
|
||||
assert safe_value_fallback(dict2, dict1, 'keyNo', 'keyNo', 1234) == 1234
|
||||
|
||||
|
||||
def test_plural() -> None:
|
||||
assert plural(0, "page") == "pages"
|
||||
assert plural(0.0, "page") == "pages"
|
||||
@@ -123,3 +146,17 @@ def test_plural() -> None:
|
||||
assert plural(1.5, "ox", "oxen") == "oxen"
|
||||
assert plural(-0.5, "ox", "oxen") == "oxen"
|
||||
assert plural(-1.5, "ox", "oxen") == "oxen"
|
||||
|
||||
|
||||
def test_render_template_fallback(mocker):
|
||||
from jinja2.exceptions import TemplateNotFound
|
||||
with pytest.raises(TemplateNotFound):
|
||||
val = render_template(
|
||||
templatefile='subtemplates/indicators_does-not-exist.j2',)
|
||||
|
||||
val = render_template_with_fallback(
|
||||
templatefile='subtemplates/indicators_does-not-exist.j2',
|
||||
templatefallbackfile='subtemplates/indicators_minimal.j2',
|
||||
)
|
||||
assert isinstance(val, str)
|
||||
assert 'if self.dp' in val
|
||||
|
@@ -9,53 +9,7 @@ from sqlalchemy import create_engine
|
||||
from freqtrade import constants
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.persistence import Trade, clean_dry_run_db, init
|
||||
from tests.conftest import log_has
|
||||
|
||||
|
||||
def create_mock_trades(fee):
|
||||
"""
|
||||
Create some fake trades ...
|
||||
"""
|
||||
# Simulate dry_run entries
|
||||
trade = Trade(
|
||||
pair='ETH/BTC',
|
||||
stake_amount=0.001,
|
||||
amount=123.0,
|
||||
fee_open=fee.return_value,
|
||||
fee_close=fee.return_value,
|
||||
open_rate=0.123,
|
||||
exchange='bittrex',
|
||||
open_order_id='dry_run_buy_12345'
|
||||
)
|
||||
Trade.session.add(trade)
|
||||
|
||||
trade = Trade(
|
||||
pair='ETC/BTC',
|
||||
stake_amount=0.001,
|
||||
amount=123.0,
|
||||
fee_open=fee.return_value,
|
||||
fee_close=fee.return_value,
|
||||
open_rate=0.123,
|
||||
close_rate=0.128,
|
||||
close_profit=0.005,
|
||||
exchange='bittrex',
|
||||
is_open=False,
|
||||
open_order_id='dry_run_sell_12345'
|
||||
)
|
||||
Trade.session.add(trade)
|
||||
|
||||
# Simulate prod entry
|
||||
trade = Trade(
|
||||
pair='ETC/BTC',
|
||||
stake_amount=0.001,
|
||||
amount=123.0,
|
||||
fee_open=fee.return_value,
|
||||
fee_close=fee.return_value,
|
||||
open_rate=0.123,
|
||||
exchange='bittrex',
|
||||
open_order_id='prod_buy_12345'
|
||||
)
|
||||
Trade.session.add(trade)
|
||||
from tests.conftest import log_has, create_mock_trades
|
||||
|
||||
|
||||
def test_init_create_session(default_conf):
|
||||
@@ -344,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
|
||||
@@ -378,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)
|
||||
@@ -476,12 +430,22 @@ def test_migrate_old(mocker, default_conf, fee):
|
||||
stake=default_conf.get("stake_amount"),
|
||||
amount=amount
|
||||
)
|
||||
insert_table_old2 = """INSERT INTO trades (exchange, pair, is_open, fee,
|
||||
open_rate, close_rate, stake_amount, amount, open_date)
|
||||
VALUES ('BITTREX', 'BTC_ETC', 0, {fee},
|
||||
0.00258580, 0.00268580, {stake}, {amount},
|
||||
'2017-11-28 12:44:24.000000')
|
||||
""".format(fee=fee.return_value,
|
||||
stake=default_conf.get("stake_amount"),
|
||||
amount=amount
|
||||
)
|
||||
engine = create_engine('sqlite://')
|
||||
mocker.patch('freqtrade.persistence.create_engine', lambda *args, **kwargs: engine)
|
||||
|
||||
# Create table using the old format
|
||||
engine.execute(create_table_old)
|
||||
engine.execute(insert_table_old)
|
||||
engine.execute(insert_table_old2)
|
||||
# Run init to test migration
|
||||
init(default_conf['db_url'], default_conf['dry_run'])
|
||||
|
||||
@@ -500,6 +464,21 @@ def test_migrate_old(mocker, default_conf, fee):
|
||||
assert trade.stop_loss == 0.0
|
||||
assert trade.initial_stop_loss == 0.0
|
||||
assert trade.open_trade_price == trade._calc_open_trade_price()
|
||||
assert trade.close_profit_abs is None
|
||||
assert trade.fee_open_cost is None
|
||||
assert trade.fee_open_currency is None
|
||||
assert trade.fee_close_cost is None
|
||||
assert trade.fee_close_currency is None
|
||||
assert trade.timeframe is None
|
||||
|
||||
trade = Trade.query.filter(Trade.id == 2).first()
|
||||
assert trade.close_rate is not None
|
||||
assert trade.is_open == 0
|
||||
assert trade.open_rate_requested is None
|
||||
assert trade.close_rate_requested is None
|
||||
assert trade.close_rate is not None
|
||||
assert pytest.approx(trade.close_profit_abs) == trade.calc_profit()
|
||||
assert trade.sell_order_status is None
|
||||
|
||||
|
||||
def test_migrate_new(mocker, default_conf, fee, caplog):
|
||||
@@ -534,11 +513,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
|
||||
@@ -576,13 +555,14 @@ 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)
|
||||
assert log_has("trying trades_bak2", caplog)
|
||||
assert log_has("Running database migration - backup available as trades_bak2", caplog)
|
||||
assert trade.open_trade_price == trade._calc_open_trade_price()
|
||||
assert trade.close_profit_abs is None
|
||||
|
||||
|
||||
def test_migrate_mid_state(mocker, default_conf, fee, caplog):
|
||||
@@ -757,18 +737,49 @@ def test_to_json(default_conf, fee):
|
||||
|
||||
assert result == {'trade_id': None,
|
||||
'pair': 'ETH/BTC',
|
||||
'is_open': None,
|
||||
'open_date_hum': '2 hours ago',
|
||||
'open_date': trade.open_date.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
'open_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,
|
||||
'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_pct': 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,
|
||||
'timeframe': None,
|
||||
'exchange': 'bittrex',
|
||||
}
|
||||
|
||||
# Simulate dry_run entries
|
||||
trade = Trade(
|
||||
@@ -790,16 +801,47 @@ 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,
|
||||
'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_pct': 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,
|
||||
'open_order_id': None,
|
||||
'open_rate_requested': None,
|
||||
'open_trade_price': 12.33075,
|
||||
'sell_reason': None,
|
||||
'sell_order_status': None,
|
||||
'strategy': None,
|
||||
'ticker_interval': None,
|
||||
'timeframe': None,
|
||||
'exchange': 'bittrex',
|
||||
}
|
||||
|
||||
|
||||
def test_stoploss_reinitialization(default_conf, fee):
|
||||
@@ -862,6 +904,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 +989,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 +1004,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
|
||||
|
@@ -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'
|
||||
@@ -266,7 +266,7 @@ def test_generate_profit_graph(testdatadir):
|
||||
filename = testdatadir / "backtest-result_test.json"
|
||||
trades = load_backtest_data(filename)
|
||||
timerange = TimeRange.parse_timerange("20180110-20180112")
|
||||
pairs = ["TRX/BTC", "ADA/BTC"]
|
||||
pairs = ["TRX/BTC", "XLM/BTC"]
|
||||
trades = trades[trades['close_time'] < pd.Timestamp('2018-01-12', tz='UTC')]
|
||||
|
||||
data = history.load_data(datadir=testdatadir,
|
||||
@@ -292,7 +292,7 @@ def test_generate_profit_graph(testdatadir):
|
||||
|
||||
profit = find_trace_in_fig_data(figure.data, "Profit")
|
||||
assert isinstance(profit, go.Scatter)
|
||||
profit = find_trace_in_fig_data(figure.data, "Max drawdown 0.00%")
|
||||
profit = find_trace_in_fig_data(figure.data, "Max drawdown 10.45%")
|
||||
assert isinstance(profit, go.Scatter)
|
||||
|
||||
for pair in pairs:
|
||||
@@ -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
|
||||
|
BIN
tests/testdata/XRP_ETH-trades.json.gz
vendored
BIN
tests/testdata/XRP_ETH-trades.json.gz
vendored
Binary file not shown.
BIN
tests/testdata/XRP_OLD-trades.json.gz
vendored
Normal file
BIN
tests/testdata/XRP_OLD-trades.json.gz
vendored
Normal file
Binary file not shown.
7
tests/testdata/backtest-result_test copy.json
vendored
Normal file
7
tests/testdata/backtest-result_test copy.json
vendored
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user