Merge branch 'develop' into pr/froggleston/7861
This commit is contained in:
@@ -14,7 +14,8 @@ from freqtrade.commands import (start_backtesting_show, start_convert_data, star
|
||||
start_hyperopt_show, start_install_ui, start_list_data,
|
||||
start_list_exchanges, start_list_markets, start_list_strategies,
|
||||
start_list_timeframes, start_new_strategy, start_show_trades,
|
||||
start_test_pairlist, start_trading, start_webserver)
|
||||
start_strategy_update, start_test_pairlist, start_trading,
|
||||
start_webserver)
|
||||
from freqtrade.commands.db_commands import start_convert_db
|
||||
from freqtrade.commands.deploy_commands import (clean_ui_subdir, download_and_install_ui,
|
||||
get_ui_download_url, read_ui_version)
|
||||
@@ -24,7 +25,7 @@ from freqtrade.enums import RunMode
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.persistence.models import init_db
|
||||
from freqtrade.persistence.pairlock_middleware import PairLocks
|
||||
from tests.conftest import (CURRENT_TEST_STRATEGY, create_mock_trades, get_args, log_has,
|
||||
from tests.conftest import (CURRENT_TEST_STRATEGY, EXMS, create_mock_trades, get_args, log_has,
|
||||
log_has_re, patch_exchange, patched_configuration_load_config_file)
|
||||
from tests.conftest_trades import MOCK_TRADE_COUNT
|
||||
|
||||
@@ -454,7 +455,7 @@ def test_list_markets(mocker, markets_static, capsys):
|
||||
assert re.search(r"^BLK/BTC$", captured.out, re.MULTILINE)
|
||||
assert re.search(r"^LTC/USD$", captured.out, re.MULTILINE)
|
||||
|
||||
mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(side_effect=ValueError))
|
||||
mocker.patch(f'{EXMS}.markets', PropertyMock(side_effect=ValueError))
|
||||
# Test --one-column
|
||||
args = [
|
||||
"list-markets",
|
||||
@@ -643,9 +644,7 @@ def test_download_data_keyboardInterrupt(mocker, markets):
|
||||
dl_mock = mocker.patch('freqtrade.commands.data_commands.refresh_backtest_ohlcv_data',
|
||||
MagicMock(side_effect=KeyboardInterrupt))
|
||||
patch_exchange(mocker)
|
||||
mocker.patch(
|
||||
'freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets)
|
||||
)
|
||||
mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets))
|
||||
args = [
|
||||
"download-data",
|
||||
"--exchange", "binance",
|
||||
@@ -664,9 +663,7 @@ def test_download_data_timerange(mocker, markets):
|
||||
dl_mock = mocker.patch('freqtrade.commands.data_commands.refresh_backtest_ohlcv_data',
|
||||
MagicMock(return_value=["ETH/BTC", "XRP/BTC"]))
|
||||
patch_exchange(mocker)
|
||||
mocker.patch(
|
||||
'freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets)
|
||||
)
|
||||
mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets))
|
||||
args = [
|
||||
"download-data",
|
||||
"--exchange", "binance",
|
||||
@@ -715,9 +712,7 @@ def test_download_data_no_markets(mocker, caplog):
|
||||
dl_mock = mocker.patch('freqtrade.commands.data_commands.refresh_backtest_ohlcv_data',
|
||||
MagicMock(return_value=["ETH/BTC", "XRP/BTC"]))
|
||||
patch_exchange(mocker, id='binance')
|
||||
mocker.patch(
|
||||
'freqtrade.exchange.Exchange.markets', PropertyMock(return_value={})
|
||||
)
|
||||
mocker.patch(f'{EXMS}.markets', PropertyMock(return_value={}))
|
||||
args = [
|
||||
"download-data",
|
||||
"--exchange", "binance",
|
||||
@@ -733,9 +728,7 @@ def test_download_data_no_exchange(mocker, caplog):
|
||||
mocker.patch('freqtrade.commands.data_commands.refresh_backtest_ohlcv_data',
|
||||
MagicMock(return_value=["ETH/BTC", "XRP/BTC"]))
|
||||
patch_exchange(mocker)
|
||||
mocker.patch(
|
||||
'freqtrade.exchange.Exchange.markets', PropertyMock(return_value={})
|
||||
)
|
||||
mocker.patch(f'{EXMS}.markets', PropertyMock(return_value={}))
|
||||
args = [
|
||||
"download-data",
|
||||
]
|
||||
@@ -746,16 +739,12 @@ def test_download_data_no_exchange(mocker, caplog):
|
||||
start_download_data(pargs)
|
||||
|
||||
|
||||
def test_download_data_no_pairs(mocker, caplog):
|
||||
|
||||
mocker.patch.object(Path, "exists", MagicMock(return_value=False))
|
||||
def test_download_data_no_pairs(mocker):
|
||||
|
||||
mocker.patch('freqtrade.commands.data_commands.refresh_backtest_ohlcv_data',
|
||||
MagicMock(return_value=["ETH/BTC", "XRP/BTC"]))
|
||||
patch_exchange(mocker)
|
||||
mocker.patch(
|
||||
'freqtrade.exchange.Exchange.markets', PropertyMock(return_value={})
|
||||
)
|
||||
mocker.patch(f'{EXMS}.markets', PropertyMock(return_value={}))
|
||||
args = [
|
||||
"download-data",
|
||||
"--exchange",
|
||||
@@ -770,14 +759,10 @@ def test_download_data_no_pairs(mocker, caplog):
|
||||
|
||||
def test_download_data_all_pairs(mocker, markets):
|
||||
|
||||
mocker.patch.object(Path, "exists", MagicMock(return_value=False))
|
||||
|
||||
dl_mock = mocker.patch('freqtrade.commands.data_commands.refresh_backtest_ohlcv_data',
|
||||
MagicMock(return_value=["ETH/BTC", "XRP/BTC"]))
|
||||
patch_exchange(mocker)
|
||||
mocker.patch(
|
||||
'freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets)
|
||||
)
|
||||
mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets))
|
||||
args = [
|
||||
"download-data",
|
||||
"--exchange",
|
||||
@@ -814,9 +799,7 @@ def test_download_data_trades(mocker, caplog):
|
||||
convert_mock = mocker.patch('freqtrade.commands.data_commands.convert_trades_to_ohlcv',
|
||||
MagicMock(return_value=[]))
|
||||
patch_exchange(mocker)
|
||||
mocker.patch(
|
||||
'freqtrade.exchange.Exchange.markets', PropertyMock(return_value={})
|
||||
)
|
||||
mocker.patch(f'{EXMS}.markets', PropertyMock(return_value={}))
|
||||
args = [
|
||||
"download-data",
|
||||
"--exchange", "kraken",
|
||||
@@ -847,9 +830,7 @@ def test_download_data_trades(mocker, caplog):
|
||||
|
||||
def test_download_data_data_invalid(mocker):
|
||||
patch_exchange(mocker, id="kraken")
|
||||
mocker.patch(
|
||||
'freqtrade.exchange.Exchange.markets', PropertyMock(return_value={})
|
||||
)
|
||||
mocker.patch(f'{EXMS}.markets', PropertyMock(return_value={}))
|
||||
args = [
|
||||
"download-data",
|
||||
"--exchange", "kraken",
|
||||
@@ -866,9 +847,7 @@ def test_start_convert_trades(mocker, caplog):
|
||||
convert_mock = mocker.patch('freqtrade.commands.data_commands.convert_trades_to_ohlcv',
|
||||
MagicMock(return_value=[]))
|
||||
patch_exchange(mocker)
|
||||
mocker.patch(
|
||||
'freqtrade.exchange.Exchange.markets', PropertyMock(return_value={})
|
||||
)
|
||||
mocker.patch(f'{EXMS}.markets', PropertyMock(return_value={}))
|
||||
args = [
|
||||
"trades-to-ohlcv",
|
||||
"--exchange", "kraken",
|
||||
@@ -975,7 +954,7 @@ def test_start_list_freqAI_models(capsys):
|
||||
|
||||
def test_start_test_pairlist(mocker, caplog, tickers, default_conf, capsys):
|
||||
patch_exchange(mocker, mock_markets=True)
|
||||
mocker.patch.multiple('freqtrade.exchange.Exchange',
|
||||
mocker.patch.multiple(EXMS,
|
||||
exchange_has=MagicMock(return_value=True),
|
||||
get_tickers=tickers,
|
||||
)
|
||||
@@ -1454,10 +1433,10 @@ def test_start_list_data(testdatadir, capsys):
|
||||
start_list_data(pargs)
|
||||
captured = capsys.readouterr()
|
||||
|
||||
assert "Found 5 pair / timeframe combinations." in captured.out
|
||||
assert "\n| Pair | Timeframe | Type |\n" in captured.out
|
||||
assert "\n| XRP/USDT | 1h | futures |\n" in captured.out
|
||||
assert "\n| XRP/USDT | 1h, 8h | mark |\n" in captured.out
|
||||
assert "Found 6 pair / timeframe combinations." in captured.out
|
||||
assert "\n| Pair | Timeframe | Type |\n" in captured.out
|
||||
assert "\n| XRP/USDT:USDT | 5m, 1h | futures |\n" in captured.out
|
||||
assert "\n| XRP/USDT:USDT | 1h, 8h | mark |\n" in captured.out
|
||||
|
||||
args = [
|
||||
"list-data",
|
||||
@@ -1529,7 +1508,7 @@ def test_backtesting_show(mocker, testdatadir, capsys):
|
||||
args = [
|
||||
"backtesting-show",
|
||||
"--export-filename",
|
||||
f"{testdatadir / 'backtest_results/backtest-result_new.json'}",
|
||||
f"{testdatadir / 'backtest_results/backtest-result.json'}",
|
||||
"--show-pair-list"
|
||||
]
|
||||
pargs = get_args(args)
|
||||
@@ -1568,3 +1547,37 @@ def test_start_convert_db(mocker, fee, tmpdir, caplog):
|
||||
start_convert_db(pargs)
|
||||
|
||||
assert db_target_file.is_file()
|
||||
|
||||
|
||||
def test_start_strategy_updater(mocker, tmpdir):
|
||||
sc_mock = mocker.patch('freqtrade.commands.strategy_utils_commands.start_conversion')
|
||||
teststrats = Path(__file__).parent.parent / 'strategy/strats'
|
||||
args = [
|
||||
"strategy-updater",
|
||||
"--userdir",
|
||||
str(tmpdir),
|
||||
"--strategy-path",
|
||||
str(teststrats),
|
||||
]
|
||||
pargs = get_args(args)
|
||||
pargs['config'] = None
|
||||
start_strategy_update(pargs)
|
||||
# Number of strategies in the test directory
|
||||
assert sc_mock.call_count == 11
|
||||
|
||||
sc_mock.reset_mock()
|
||||
args = [
|
||||
"strategy-updater",
|
||||
"--userdir",
|
||||
str(tmpdir),
|
||||
"--strategy-path",
|
||||
str(teststrats),
|
||||
"--strategy-list",
|
||||
"StrategyTestV3",
|
||||
"StrategyTestV2"
|
||||
]
|
||||
pargs = get_args(args)
|
||||
pargs['config'] = None
|
||||
start_strategy_update(pargs)
|
||||
# Number of strategies in the test directory
|
||||
assert sc_mock.call_count == 2
|
||||
|
@@ -40,6 +40,7 @@ np.seterr(all='raise')
|
||||
|
||||
CURRENT_TEST_STRATEGY = 'StrategyTestV3'
|
||||
TRADE_SIDES = ('long', 'short')
|
||||
EXMS = 'freqtrade.exchange.exchange.Exchange'
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
@@ -145,22 +146,21 @@ def patch_exchange(
|
||||
mock_markets=True,
|
||||
mock_supported_modes=True
|
||||
) -> None:
|
||||
mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock(return_value={}))
|
||||
mocker.patch('freqtrade.exchange.Exchange.validate_config', MagicMock())
|
||||
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
||||
mocker.patch('freqtrade.exchange.Exchange.id', PropertyMock(return_value=id))
|
||||
mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value=id.title()))
|
||||
mocker.patch('freqtrade.exchange.Exchange.precisionMode', PropertyMock(return_value=2))
|
||||
mocker.patch(f'{EXMS}._load_async_markets', return_value={})
|
||||
mocker.patch(f'{EXMS}.validate_config', MagicMock())
|
||||
mocker.patch(f'{EXMS}.validate_timeframes', MagicMock())
|
||||
mocker.patch(f'{EXMS}.id', PropertyMock(return_value=id))
|
||||
mocker.patch(f'{EXMS}.name', PropertyMock(return_value=id.title()))
|
||||
mocker.patch(f'{EXMS}.precisionMode', PropertyMock(return_value=2))
|
||||
|
||||
if mock_markets:
|
||||
if isinstance(mock_markets, bool):
|
||||
mock_markets = get_markets()
|
||||
mocker.patch('freqtrade.exchange.Exchange.markets',
|
||||
PropertyMock(return_value=mock_markets))
|
||||
mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=mock_markets))
|
||||
|
||||
if mock_supported_modes:
|
||||
mocker.patch(
|
||||
f'freqtrade.exchange.{id.capitalize()}._supported_trading_mode_margin_pairs',
|
||||
f'freqtrade.exchange.{id}.{id.capitalize()}._supported_trading_mode_margin_pairs',
|
||||
PropertyMock(return_value=[
|
||||
(TradingMode.MARGIN, MarginMode.CROSS),
|
||||
(TradingMode.MARGIN, MarginMode.ISOLATED),
|
||||
@@ -170,10 +170,10 @@ def patch_exchange(
|
||||
)
|
||||
|
||||
if api_mock:
|
||||
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||
mocker.patch(f'{EXMS}._init_ccxt', return_value=api_mock)
|
||||
else:
|
||||
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock())
|
||||
mocker.patch('freqtrade.exchange.Exchange.timeframes', PropertyMock(
|
||||
mocker.patch(f'{EXMS}._init_ccxt', MagicMock())
|
||||
mocker.patch(f'{EXMS}.timeframes', PropertyMock(
|
||||
return_value=['5m', '15m', '1h', '1d']))
|
||||
|
||||
|
||||
@@ -241,7 +241,6 @@ def get_patched_freqtradebot(mocker, config) -> FreqtradeBot:
|
||||
:return: FreqtradeBot
|
||||
"""
|
||||
patch_freqtradebot(mocker, config)
|
||||
config['datadir'] = Path(config['datadir'])
|
||||
return FreqtradeBot(config)
|
||||
|
||||
|
||||
@@ -300,7 +299,7 @@ def create_mock_trades(fee, is_short: Optional[bool] = False, use_db: bool = Tru
|
||||
"""
|
||||
def add_trade(trade):
|
||||
if use_db:
|
||||
Trade.query.session.add(trade)
|
||||
Trade.session.add(trade)
|
||||
else:
|
||||
LocalTrade.add_bt_trade(trade)
|
||||
is_short1 = is_short if is_short is not None else True
|
||||
@@ -333,11 +332,11 @@ def create_mock_trades_with_leverage(fee, use_db: bool = True):
|
||||
Create some fake trades ...
|
||||
"""
|
||||
if use_db:
|
||||
Trade.query.session.rollback()
|
||||
Trade.session.rollback()
|
||||
|
||||
def add_trade(trade):
|
||||
if use_db:
|
||||
Trade.query.session.add(trade)
|
||||
Trade.session.add(trade)
|
||||
else:
|
||||
LocalTrade.add_bt_trade(trade)
|
||||
|
||||
@@ -367,7 +366,7 @@ def create_mock_trades_with_leverage(fee, use_db: bool = True):
|
||||
add_trade(trade)
|
||||
|
||||
if use_db:
|
||||
Trade.query.session.flush()
|
||||
Trade.session.flush()
|
||||
|
||||
|
||||
def create_mock_trades_usdt(fee, is_short: Optional[bool] = False, use_db: bool = True):
|
||||
@@ -376,7 +375,7 @@ def create_mock_trades_usdt(fee, is_short: Optional[bool] = False, use_db: bool
|
||||
"""
|
||||
def add_trade(trade):
|
||||
if use_db:
|
||||
Trade.query.session.add(trade)
|
||||
Trade.session.add(trade)
|
||||
else:
|
||||
LocalTrade.add_bt_trade(trade)
|
||||
|
||||
@@ -408,6 +407,11 @@ def create_mock_trades_usdt(fee, is_short: Optional[bool] = False, use_db: bool
|
||||
Trade.commit()
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def patch_gc(mocker) -> None:
|
||||
mocker.patch("freqtrade.main.gc_set_threshold")
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def patch_coingekko(mocker) -> None:
|
||||
"""
|
||||
@@ -505,7 +509,7 @@ def get_default_conf(testdatadir):
|
||||
"chat_id": "0",
|
||||
"notification_settings": {},
|
||||
},
|
||||
"datadir": str(testdatadir),
|
||||
"datadir": Path(testdatadir),
|
||||
"initial_state": "running",
|
||||
"db_url": "sqlite://",
|
||||
"user_data_dir": Path("user_data"),
|
||||
@@ -2569,7 +2573,7 @@ def import_fails() -> None:
|
||||
realimport = builtins.__import__
|
||||
|
||||
def mockedimport(name, *args, **kwargs):
|
||||
if name in ["filelock", 'systemd.journal', 'uvloop']:
|
||||
if name in ["filelock", 'cysystemd.journal', 'uvloop']:
|
||||
raise ImportError(f"No module named '{name}'")
|
||||
return realimport(name, *args, **kwargs)
|
||||
|
||||
@@ -2601,6 +2605,8 @@ def open_trade():
|
||||
ft_order_side='buy',
|
||||
ft_pair=trade.pair,
|
||||
ft_is_open=False,
|
||||
ft_amount=trade.amount,
|
||||
ft_price=trade.open_rate,
|
||||
order_id='123456789',
|
||||
status="closed",
|
||||
symbol=trade.pair,
|
||||
@@ -2637,6 +2643,8 @@ def open_trade_usdt():
|
||||
ft_order_side='buy',
|
||||
ft_pair=trade.pair,
|
||||
ft_is_open=False,
|
||||
ft_amount=trade.amount,
|
||||
ft_price=trade.open_rate,
|
||||
order_id='123456789',
|
||||
status="closed",
|
||||
symbol=trade.pair,
|
||||
@@ -2654,6 +2662,8 @@ def open_trade_usdt():
|
||||
ft_order_side='exit',
|
||||
ft_pair=trade.pair,
|
||||
ft_is_open=True,
|
||||
ft_amount=trade.amount,
|
||||
ft_price=trade.open_rate,
|
||||
order_id='123456789_exit',
|
||||
status="open",
|
||||
symbol=trade.pair,
|
||||
@@ -3098,7 +3108,7 @@ def funding_rate_history_octohourly():
|
||||
@pytest.fixture(scope='function')
|
||||
def leverage_tiers():
|
||||
return {
|
||||
"1000SHIB/USDT": [
|
||||
"1000SHIB/USDT:USDT": [
|
||||
{
|
||||
'minNotional': 0,
|
||||
'maxNotional': 50000,
|
||||
@@ -3149,7 +3159,7 @@ def leverage_tiers():
|
||||
'maintAmt': 654500.0
|
||||
},
|
||||
],
|
||||
"1INCH/USDT": [
|
||||
"1INCH/USDT:USDT": [
|
||||
{
|
||||
'minNotional': 0,
|
||||
'maxNotional': 5000,
|
||||
@@ -3193,7 +3203,7 @@ def leverage_tiers():
|
||||
'maintAmt': 386940.0
|
||||
},
|
||||
],
|
||||
"AAVE/USDT": [
|
||||
"AAVE/USDT:USDT": [
|
||||
{
|
||||
'minNotional': 0,
|
||||
'maxNotional': 5000,
|
||||
@@ -3237,7 +3247,7 @@ def leverage_tiers():
|
||||
'maintAmt': 386950.0
|
||||
},
|
||||
],
|
||||
"ADA/BUSD": [
|
||||
"ADA/BUSD:BUSD": [
|
||||
{
|
||||
"minNotional": 0,
|
||||
"maxNotional": 100000,
|
||||
@@ -3281,7 +3291,7 @@ def leverage_tiers():
|
||||
"maintAmt": 1527500.0
|
||||
},
|
||||
],
|
||||
'BNB/BUSD': [
|
||||
'BNB/BUSD:BUSD': [
|
||||
{
|
||||
"minNotional": 0, # stake(before leverage) = 0
|
||||
"maxNotional": 100000, # max stake(before leverage) = 5000
|
||||
@@ -3325,7 +3335,7 @@ def leverage_tiers():
|
||||
"maintAmt": 1527500.0
|
||||
}
|
||||
],
|
||||
'BNB/USDT': [
|
||||
'BNB/USDT:USDT': [
|
||||
{
|
||||
"minNotional": 0, # stake = 0.0
|
||||
"maxNotional": 10000, # max_stake = 133.33333333333334
|
||||
@@ -3390,7 +3400,7 @@ def leverage_tiers():
|
||||
"maintAmt": 6233035.0
|
||||
},
|
||||
],
|
||||
'BTC/USDT': [
|
||||
'BTC/USDT:USDT': [
|
||||
{
|
||||
"minNotional": 0, # stake = 0.0
|
||||
"maxNotional": 50000, # max_stake = 400.0
|
||||
@@ -3462,7 +3472,7 @@ def leverage_tiers():
|
||||
"maintAmt": 1.997038E8
|
||||
},
|
||||
],
|
||||
"ZEC/USDT": [
|
||||
"ZEC/USDT:USDT": [
|
||||
{
|
||||
'minNotional': 0,
|
||||
'maxNotional': 50000,
|
||||
|
@@ -12,9 +12,11 @@ from freqtrade.data.btanalysis import (BT_DATA_COLUMNS, analyze_trade_parallelis
|
||||
get_latest_hyperopt_file, load_backtest_data,
|
||||
load_backtest_metadata, load_trades, load_trades_from_db)
|
||||
from freqtrade.data.history import load_data, load_pair_history
|
||||
from freqtrade.data.metrics import (calculate_cagr, calculate_csum, calculate_market_change,
|
||||
calculate_max_drawdown, calculate_underwater,
|
||||
combine_dataframes_with_mean, create_cum_profit)
|
||||
from freqtrade.data.metrics import (calculate_cagr, calculate_calmar, calculate_csum,
|
||||
calculate_expectancy, calculate_market_change,
|
||||
calculate_max_drawdown, calculate_sharpe, calculate_sortino,
|
||||
calculate_underwater, combine_dataframes_with_mean,
|
||||
create_cum_profit)
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from tests.conftest import CURRENT_TEST_STRATEGY, create_mock_trades
|
||||
from tests.conftest_trades import MOCK_TRADE_COUNT
|
||||
@@ -30,10 +32,10 @@ def test_get_latest_backtest_filename(testdatadir, mocker):
|
||||
|
||||
testdir_bt = testdatadir / "backtest_results"
|
||||
res = get_latest_backtest_filename(testdir_bt)
|
||||
assert res == 'backtest-result_new.json'
|
||||
assert res == 'backtest-result.json'
|
||||
|
||||
res = get_latest_backtest_filename(str(testdir_bt))
|
||||
assert res == 'backtest-result_new.json'
|
||||
assert res == 'backtest-result.json'
|
||||
|
||||
mocker.patch("freqtrade.data.btanalysis.json_load", return_value={})
|
||||
|
||||
@@ -81,7 +83,7 @@ def test_load_backtest_data_old_format(testdatadir, mocker):
|
||||
|
||||
def test_load_backtest_data_new_format(testdatadir):
|
||||
|
||||
filename = testdatadir / "backtest_results/backtest-result_new.json"
|
||||
filename = testdatadir / "backtest_results/backtest-result.json"
|
||||
bt_data = load_backtest_data(filename)
|
||||
assert isinstance(bt_data, DataFrame)
|
||||
assert set(bt_data.columns) == set(BT_DATA_COLUMNS)
|
||||
@@ -182,7 +184,7 @@ def test_extract_trades_of_period(testdatadir):
|
||||
|
||||
|
||||
def test_analyze_trade_parallelism(testdatadir):
|
||||
filename = testdatadir / "backtest_results/backtest-result_new.json"
|
||||
filename = testdatadir / "backtest_results/backtest-result.json"
|
||||
bt_data = load_backtest_data(filename)
|
||||
|
||||
res = analyze_trade_parallelism(bt_data, "5m")
|
||||
@@ -256,7 +258,7 @@ def test_combine_dataframes_with_mean_no_data(testdatadir):
|
||||
|
||||
|
||||
def test_create_cum_profit(testdatadir):
|
||||
filename = testdatadir / "backtest_results/backtest-result_new.json"
|
||||
filename = testdatadir / "backtest_results/backtest-result.json"
|
||||
bt_data = load_backtest_data(filename)
|
||||
timerange = TimeRange.parse_timerange("20180110-20180112")
|
||||
|
||||
@@ -268,11 +270,11 @@ def test_create_cum_profit(testdatadir):
|
||||
"cum_profits", timeframe="5m")
|
||||
assert "cum_profits" in cum_profits.columns
|
||||
assert cum_profits.iloc[0]['cum_profits'] == 0
|
||||
assert pytest.approx(cum_profits.iloc[-1]['cum_profits']) == 8.723007518796964e-06
|
||||
assert pytest.approx(cum_profits.iloc[-1]['cum_profits']) == 9.0225563e-05
|
||||
|
||||
|
||||
def test_create_cum_profit1(testdatadir):
|
||||
filename = testdatadir / "backtest_results/backtest-result_new.json"
|
||||
filename = testdatadir / "backtest_results/backtest-result.json"
|
||||
bt_data = load_backtest_data(filename)
|
||||
# Move close-time to "off" the candle, to make sure the logic still works
|
||||
bt_data['close_date'] = bt_data.loc[:, 'close_date'] + DateOffset(seconds=20)
|
||||
@@ -286,7 +288,7 @@ def test_create_cum_profit1(testdatadir):
|
||||
"cum_profits", timeframe="5m")
|
||||
assert "cum_profits" in cum_profits.columns
|
||||
assert cum_profits.iloc[0]['cum_profits'] == 0
|
||||
assert pytest.approx(cum_profits.iloc[-1]['cum_profits']) == 8.723007518796964e-06
|
||||
assert pytest.approx(cum_profits.iloc[-1]['cum_profits']) == 9.0225563e-05
|
||||
|
||||
with pytest.raises(ValueError, match='Trade dataframe empty.'):
|
||||
create_cum_profit(df.set_index('date'), bt_data[bt_data["pair"] == 'NOTAPAIR'],
|
||||
@@ -294,18 +296,18 @@ def test_create_cum_profit1(testdatadir):
|
||||
|
||||
|
||||
def test_calculate_max_drawdown(testdatadir):
|
||||
filename = testdatadir / "backtest_results/backtest-result_new.json"
|
||||
filename = testdatadir / "backtest_results/backtest-result.json"
|
||||
bt_data = load_backtest_data(filename)
|
||||
_, hdate, lowdate, hval, lval, drawdown = calculate_max_drawdown(
|
||||
bt_data, value_col="profit_abs")
|
||||
assert isinstance(drawdown, float)
|
||||
assert pytest.approx(drawdown) == 0.12071099
|
||||
assert pytest.approx(drawdown) == 0.29753914
|
||||
assert isinstance(hdate, Timestamp)
|
||||
assert isinstance(lowdate, Timestamp)
|
||||
assert isinstance(hval, float)
|
||||
assert isinstance(lval, float)
|
||||
assert hdate == Timestamp('2018-01-25 01:30:00', tz='UTC')
|
||||
assert lowdate == Timestamp('2018-01-25 03:50:00', tz='UTC')
|
||||
assert hdate == Timestamp('2018-01-16 19:30:00', tz='UTC')
|
||||
assert lowdate == Timestamp('2018-01-16 22:25:00', tz='UTC')
|
||||
|
||||
underwater = calculate_underwater(bt_data)
|
||||
assert isinstance(underwater, DataFrame)
|
||||
@@ -318,14 +320,15 @@ def test_calculate_max_drawdown(testdatadir):
|
||||
|
||||
|
||||
def test_calculate_csum(testdatadir):
|
||||
filename = testdatadir / "backtest_results/backtest-result_new.json"
|
||||
filename = testdatadir / "backtest_results/backtest-result.json"
|
||||
bt_data = load_backtest_data(filename)
|
||||
csum_min, csum_max = calculate_csum(bt_data)
|
||||
|
||||
assert isinstance(csum_min, float)
|
||||
assert isinstance(csum_max, float)
|
||||
assert csum_min < 0.01
|
||||
assert csum_max > 0.02
|
||||
assert csum_min < csum_max
|
||||
assert csum_min < 0.0001
|
||||
assert csum_max > 0.0002
|
||||
csum_min1, csum_max1 = calculate_csum(bt_data, 5)
|
||||
|
||||
assert csum_min1 == csum_min + 5
|
||||
@@ -335,6 +338,69 @@ def test_calculate_csum(testdatadir):
|
||||
csum_min, csum_max = calculate_csum(DataFrame())
|
||||
|
||||
|
||||
def test_calculate_expectancy(testdatadir):
|
||||
filename = testdatadir / "backtest_results/backtest-result.json"
|
||||
bt_data = load_backtest_data(filename)
|
||||
|
||||
expectancy = calculate_expectancy(DataFrame())
|
||||
assert expectancy == 0.0
|
||||
|
||||
expectancy = calculate_expectancy(bt_data)
|
||||
assert isinstance(expectancy, float)
|
||||
assert pytest.approx(expectancy) == 0.07151374226574791
|
||||
|
||||
|
||||
def test_calculate_sortino(testdatadir):
|
||||
filename = testdatadir / "backtest_results/backtest-result.json"
|
||||
bt_data = load_backtest_data(filename)
|
||||
|
||||
sortino = calculate_sortino(DataFrame(), None, None, 0)
|
||||
assert sortino == 0.0
|
||||
|
||||
sortino = calculate_sortino(
|
||||
bt_data,
|
||||
bt_data['open_date'].min(),
|
||||
bt_data['close_date'].max(),
|
||||
0.01,
|
||||
)
|
||||
assert isinstance(sortino, float)
|
||||
assert pytest.approx(sortino) == 35.17722
|
||||
|
||||
|
||||
def test_calculate_sharpe(testdatadir):
|
||||
filename = testdatadir / "backtest_results/backtest-result.json"
|
||||
bt_data = load_backtest_data(filename)
|
||||
|
||||
sharpe = calculate_sharpe(DataFrame(), None, None, 0)
|
||||
assert sharpe == 0.0
|
||||
|
||||
sharpe = calculate_sharpe(
|
||||
bt_data,
|
||||
bt_data['open_date'].min(),
|
||||
bt_data['close_date'].max(),
|
||||
0.01,
|
||||
)
|
||||
assert isinstance(sharpe, float)
|
||||
assert pytest.approx(sharpe) == 44.5078669
|
||||
|
||||
|
||||
def test_calculate_calmar(testdatadir):
|
||||
filename = testdatadir / "backtest_results/backtest-result.json"
|
||||
bt_data = load_backtest_data(filename)
|
||||
|
||||
calmar = calculate_calmar(DataFrame(), None, None, 0)
|
||||
assert calmar == 0.0
|
||||
|
||||
calmar = calculate_calmar(
|
||||
bt_data,
|
||||
bt_data['open_date'].min(),
|
||||
bt_data['close_date'].max(),
|
||||
0.01,
|
||||
)
|
||||
assert isinstance(calmar, float)
|
||||
assert pytest.approx(calmar) == 559.040508
|
||||
|
||||
|
||||
@pytest.mark.parametrize('start,end,days, expected', [
|
||||
(64900, 176000, 3 * 365, 0.3945),
|
||||
(64900, 176000, 365, 1.7119),
|
||||
|
@@ -294,8 +294,8 @@ def test_convert_trades_format(default_conf, testdatadir, tmpdir):
|
||||
|
||||
@pytest.mark.parametrize('file_base,candletype', [
|
||||
(['XRP_ETH-5m', 'XRP_ETH-1m'], CandleType.SPOT),
|
||||
(['UNITTEST_USDT-1h-mark', 'XRP_USDT-1h-mark'], CandleType.MARK),
|
||||
(['XRP_USDT-1h-futures'], CandleType.FUTURES),
|
||||
(['UNITTEST_USDT_USDT-1h-mark', 'XRP_USDT_USDT-1h-mark'], CandleType.MARK),
|
||||
(['XRP_USDT_USDT-1h-futures'], CandleType.FUTURES),
|
||||
])
|
||||
def test_convert_ohlcv_format(default_conf, testdatadir, tmpdir, file_base, candletype):
|
||||
tmpdir1 = Path(tmpdir)
|
||||
@@ -315,7 +315,10 @@ def test_convert_ohlcv_format(default_conf, testdatadir, tmpdir, file_base, cand
|
||||
files_new.append(file_new)
|
||||
|
||||
default_conf['datadir'] = tmpdir1
|
||||
default_conf['pairs'] = ['XRP_ETH', 'XRP_USDT', 'UNITTEST_USDT']
|
||||
if candletype == CandleType.SPOT:
|
||||
default_conf['pairs'] = ['XRP/ETH', 'XRP/USDT', 'UNITTEST/USDT']
|
||||
else:
|
||||
default_conf['pairs'] = ['XRP/ETH:ETH', 'XRP/USDT:USDT', 'UNITTEST/USDT:USDT']
|
||||
default_conf['timeframes'] = ['1m', '5m', '1h']
|
||||
|
||||
assert not file_new.exists()
|
||||
|
@@ -33,10 +33,10 @@ def test_datahandler_ohlcv_get_pairs(testdatadir):
|
||||
assert set(pairs) == {'UNITTEST/BTC'}
|
||||
|
||||
pairs = JsonDataHandler.ohlcv_get_pairs(testdatadir, '1h', candle_type=CandleType.MARK)
|
||||
assert set(pairs) == {'UNITTEST/USDT', 'XRP/USDT'}
|
||||
assert set(pairs) == {'UNITTEST/USDT:USDT', 'XRP/USDT:USDT'}
|
||||
|
||||
pairs = JsonGzDataHandler.ohlcv_get_pairs(testdatadir, '1h', candle_type=CandleType.FUTURES)
|
||||
assert set(pairs) == {'XRP/USDT'}
|
||||
assert set(pairs) == {'XRP/USDT:USDT'}
|
||||
|
||||
pairs = HDF5DataHandler.ohlcv_get_pairs(testdatadir, '1h', candle_type=CandleType.MARK)
|
||||
assert set(pairs) == {'UNITTEST/USDT:USDT'}
|
||||
@@ -104,11 +104,12 @@ def test_datahandler_ohlcv_get_available_data(testdatadir):
|
||||
paircombs = JsonDataHandler.ohlcv_get_available_data(testdatadir, TradingMode.FUTURES)
|
||||
# Convert to set to avoid failures due to sorting
|
||||
assert set(paircombs) == {
|
||||
('UNITTEST/USDT', '1h', 'mark'),
|
||||
('XRP/USDT', '1h', 'futures'),
|
||||
('XRP/USDT', '1h', 'mark'),
|
||||
('XRP/USDT', '8h', 'mark'),
|
||||
('XRP/USDT', '8h', 'funding_rate'),
|
||||
('UNITTEST/USDT:USDT', '1h', 'mark'),
|
||||
('XRP/USDT:USDT', '5m', 'futures'),
|
||||
('XRP/USDT:USDT', '1h', 'futures'),
|
||||
('XRP/USDT:USDT', '1h', 'mark'),
|
||||
('XRP/USDT:USDT', '8h', 'mark'),
|
||||
('XRP/USDT:USDT', '8h', 'funding_rate'),
|
||||
}
|
||||
|
||||
paircombs = JsonGzDataHandler.ohlcv_get_available_data(testdatadir, TradingMode.SPOT)
|
||||
@@ -142,7 +143,7 @@ def test_jsondatahandler_ohlcv_load(testdatadir, caplog):
|
||||
df = dh.ohlcv_load('XRP/ETH', '5m', 'spot')
|
||||
assert len(df) == 712
|
||||
|
||||
df_mark = dh.ohlcv_load('UNITTEST/USDT', '1h', candle_type="mark")
|
||||
df_mark = dh.ohlcv_load('UNITTEST/USDT:USDT', '1h', candle_type="mark")
|
||||
assert len(df_mark) == 100
|
||||
|
||||
df_no_mark = dh.ohlcv_load('UNITTEST/USDT', '1h', 'spot')
|
||||
@@ -424,7 +425,7 @@ def test_hdf5datahandler_ohlcv_load_and_resave(
|
||||
# Data goes from 2018-01-10 - 2018-01-30
|
||||
('UNITTEST/BTC', '5m', 'spot', '', '2018-01-15', '2018-01-19'),
|
||||
# Mark data goes from to 2021-11-15 2021-11-19
|
||||
('UNITTEST/USDT', '1h', 'mark', '-mark', '2021-11-16', '2021-11-18'),
|
||||
('UNITTEST/USDT:USDT', '1h', 'mark', '-mark', '2021-11-16', '2021-11-18'),
|
||||
])
|
||||
@pytest.mark.parametrize('datahandler', ['hdf5', 'feather', 'parquet'])
|
||||
def test_generic_datahandler_ohlcv_load_and_resave(
|
||||
|
@@ -2,13 +2,13 @@ from datetime import datetime, timezone
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
from pandas import DataFrame
|
||||
from pandas import DataFrame, Timestamp
|
||||
|
||||
from freqtrade.data.dataprovider import DataProvider
|
||||
from freqtrade.enums import CandleType, RunMode
|
||||
from freqtrade.exceptions import ExchangeError, OperationalException
|
||||
from freqtrade.plugins.pairlistmanager import PairListManager
|
||||
from tests.conftest import get_patched_exchange
|
||||
from tests.conftest import EXMS, generate_test_data, get_patched_exchange
|
||||
|
||||
|
||||
@pytest.mark.parametrize('candle_type', [
|
||||
@@ -144,7 +144,7 @@ def test_available_pairs(mocker, default_conf, ohlcv_history):
|
||||
assert dp.available_pairs == [("XRP/BTC", timeframe), ("UNITTEST/BTC", timeframe), ]
|
||||
|
||||
|
||||
def test_producer_pairs(mocker, default_conf, ohlcv_history):
|
||||
def test_producer_pairs(default_conf):
|
||||
dataprovider = DataProvider(default_conf, None)
|
||||
|
||||
producer = "default"
|
||||
@@ -161,9 +161,9 @@ def test_producer_pairs(mocker, default_conf, ohlcv_history):
|
||||
assert dataprovider.get_producer_pairs("bad") == []
|
||||
|
||||
|
||||
def test_get_producer_df(mocker, default_conf, ohlcv_history):
|
||||
def test_get_producer_df(default_conf):
|
||||
dataprovider = DataProvider(default_conf, None)
|
||||
|
||||
ohlcv_history = generate_test_data('5m', 150)
|
||||
pair = 'BTC/USDT'
|
||||
timeframe = default_conf['timeframe']
|
||||
candle_type = CandleType.SPOT
|
||||
@@ -221,9 +221,9 @@ def test_emit_df(mocker, default_conf, ohlcv_history):
|
||||
assert send_mock.call_count == 0
|
||||
|
||||
|
||||
def test_refresh(mocker, default_conf, ohlcv_history):
|
||||
def test_refresh(mocker, default_conf):
|
||||
refresh_mock = MagicMock()
|
||||
mocker.patch("freqtrade.exchange.Exchange.refresh_latest_ohlcv", refresh_mock)
|
||||
mocker.patch(f"{EXMS}.refresh_latest_ohlcv", refresh_mock)
|
||||
|
||||
exchange = get_patched_exchange(mocker, default_conf, id="binance")
|
||||
timeframe = default_conf["timeframe"]
|
||||
@@ -281,7 +281,7 @@ def test_market(mocker, default_conf, markets):
|
||||
|
||||
def test_ticker(mocker, default_conf, tickers):
|
||||
ticker_mock = MagicMock(return_value=tickers()['ETH/BTC'])
|
||||
mocker.patch("freqtrade.exchange.Exchange.fetch_ticker", ticker_mock)
|
||||
mocker.patch(f"{EXMS}.fetch_ticker", ticker_mock)
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
dp = DataProvider(default_conf, exchange)
|
||||
res = dp.ticker('ETH/BTC')
|
||||
@@ -290,7 +290,7 @@ def test_ticker(mocker, default_conf, tickers):
|
||||
assert res['symbol'] == 'ETH/BTC'
|
||||
|
||||
ticker_mock = MagicMock(side_effect=ExchangeError('Pair not found'))
|
||||
mocker.patch("freqtrade.exchange.Exchange.fetch_ticker", ticker_mock)
|
||||
mocker.patch(f"{EXMS}.fetch_ticker", ticker_mock)
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
dp = DataProvider(default_conf, exchange)
|
||||
res = dp.ticker('UNITTEST/BTC')
|
||||
@@ -301,7 +301,7 @@ 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',
|
||||
mocker.patch.multiple(EXMS,
|
||||
exchange_has=MagicMock(return_value=True),
|
||||
get_tickers=tickers)
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
@@ -412,3 +412,85 @@ def test_dp_send_msg(default_conf):
|
||||
dp = DataProvider(default_conf, None)
|
||||
dp.send_msg(msg, always_send=True)
|
||||
assert msg not in dp._msg_queue
|
||||
|
||||
|
||||
def test_dp__add_external_df(default_conf_usdt):
|
||||
timeframe = '1h'
|
||||
default_conf_usdt["timeframe"] = timeframe
|
||||
dp = DataProvider(default_conf_usdt, None)
|
||||
df = generate_test_data(timeframe, 24, '2022-01-01 00:00:00+00:00')
|
||||
last_analyzed = datetime.now(timezone.utc)
|
||||
|
||||
res = dp._add_external_df('ETH/USDT', df, last_analyzed, timeframe, CandleType.SPOT)
|
||||
assert res[0] is False
|
||||
# Why 1000 ??
|
||||
assert res[1] == 1000
|
||||
|
||||
# Hard add dataframe
|
||||
dp._replace_external_df('ETH/USDT', df, last_analyzed, timeframe, CandleType.SPOT)
|
||||
# BTC is not stored yet
|
||||
res = dp._add_external_df('BTC/USDT', df, last_analyzed, timeframe, CandleType.SPOT)
|
||||
assert res[0] is False
|
||||
df_res, _ = dp.get_producer_df('ETH/USDT', timeframe, CandleType.SPOT)
|
||||
assert len(df_res) == 24
|
||||
|
||||
# Add the same dataframe again - dataframe size shall not change.
|
||||
res = dp._add_external_df('ETH/USDT', df, last_analyzed, timeframe, CandleType.SPOT)
|
||||
assert res[0] is True
|
||||
assert isinstance(res[1], int)
|
||||
assert res[1] == 0
|
||||
df, _ = dp.get_producer_df('ETH/USDT', timeframe, CandleType.SPOT)
|
||||
assert len(df) == 24
|
||||
|
||||
# Add a new day.
|
||||
df2 = generate_test_data(timeframe, 24, '2022-01-02 00:00:00+00:00')
|
||||
|
||||
res = dp._add_external_df('ETH/USDT', df2, last_analyzed, timeframe, CandleType.SPOT)
|
||||
assert res[0] is True
|
||||
assert isinstance(res[1], int)
|
||||
assert res[1] == 0
|
||||
df, _ = dp.get_producer_df('ETH/USDT', timeframe, CandleType.SPOT)
|
||||
assert len(df) == 48
|
||||
|
||||
# Add a dataframe with a 12 hour offset - so 12 candles are overlapping, and 12 valid.
|
||||
df3 = generate_test_data(timeframe, 24, '2022-01-02 12:00:00+00:00')
|
||||
|
||||
res = dp._add_external_df('ETH/USDT', df3, last_analyzed, timeframe, CandleType.SPOT)
|
||||
assert res[0] is True
|
||||
assert isinstance(res[1], int)
|
||||
assert res[1] == 0
|
||||
df, _ = dp.get_producer_df('ETH/USDT', timeframe, CandleType.SPOT)
|
||||
# New length = 48 + 12 (since we have a 12 hour offset).
|
||||
assert len(df) == 60
|
||||
assert df.iloc[-1]['date'] == df3.iloc[-1]['date']
|
||||
assert df.iloc[-1]['date'] == Timestamp('2022-01-03 11:00:00+00:00')
|
||||
|
||||
# Generate 1 new candle
|
||||
df4 = generate_test_data(timeframe, 1, '2022-01-03 12:00:00+00:00')
|
||||
res = dp._add_external_df('ETH/USDT', df4, last_analyzed, timeframe, CandleType.SPOT)
|
||||
# assert res[0] is True
|
||||
# assert res[1] == 0
|
||||
df, _ = dp.get_producer_df('ETH/USDT', timeframe, CandleType.SPOT)
|
||||
# New length = 61 + 1
|
||||
assert len(df) == 61
|
||||
assert df.iloc[-2]['date'] == Timestamp('2022-01-03 11:00:00+00:00')
|
||||
assert df.iloc[-1]['date'] == Timestamp('2022-01-03 12:00:00+00:00')
|
||||
|
||||
# Gap in the data ...
|
||||
df4 = generate_test_data(timeframe, 1, '2022-01-05 00:00:00+00:00')
|
||||
res = dp._add_external_df('ETH/USDT', df4, last_analyzed, timeframe, CandleType.SPOT)
|
||||
assert res[0] is False
|
||||
# 36 hours - from 2022-01-03 12:00:00+00:00 to 2022-01-05 00:00:00+00:00
|
||||
assert isinstance(res[1], int)
|
||||
assert res[1] == 36
|
||||
df, _ = dp.get_producer_df('ETH/USDT', timeframe, CandleType.SPOT)
|
||||
# New length = 61 + 1
|
||||
assert len(df) == 61
|
||||
|
||||
# Empty dataframe
|
||||
df4 = generate_test_data(timeframe, 0, '2022-01-05 00:00:00+00:00')
|
||||
res = dp._add_external_df('ETH/USDT', df4, last_analyzed, timeframe, CandleType.SPOT)
|
||||
assert res[0] is False
|
||||
# 36 hours - from 2022-01-03 12:00:00+00:00 to 2022-01-05 00:00:00+00:00
|
||||
assert isinstance(res[1], int)
|
||||
assert res[1] == 0
|
||||
|
9
tests/data/test_entryexitanalysis.py
Executable file → Normal file
9
tests/data/test_entryexitanalysis.py
Executable file → Normal file
@@ -190,6 +190,15 @@ def test_backtest_analysis_nomock(default_conf, mocker, caplog, testdatadir, tmp
|
||||
assert '1' in captured.out
|
||||
assert '2.5' in captured.out
|
||||
|
||||
# test group 5
|
||||
args = get_args(base_args + ['--analysis-groups', "5"])
|
||||
start_analysis_entries_exits(args)
|
||||
captured = capsys.readouterr()
|
||||
assert 'exit_signal' in captured.out
|
||||
assert 'roi' in captured.out
|
||||
assert 'stop_loss' in captured.out
|
||||
assert 'trailing_stop_loss' in captured.out
|
||||
|
||||
# test date filtering
|
||||
args = get_args(base_args +
|
||||
['--analysis-groups', "0", "1", "2",
|
||||
|
@@ -26,7 +26,7 @@ from freqtrade.enums import CandleType
|
||||
from freqtrade.exchange import timeframe_to_minutes
|
||||
from freqtrade.misc import file_dump_json
|
||||
from freqtrade.resolvers import StrategyResolver
|
||||
from tests.conftest import (CURRENT_TEST_STRATEGY, get_patched_exchange, log_has, log_has_re,
|
||||
from tests.conftest import (CURRENT_TEST_STRATEGY, EXMS, get_patched_exchange, log_has, log_has_re,
|
||||
patch_exchange)
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ def test_load_data_7min_timeframe(caplog, testdatadir) -> None:
|
||||
|
||||
|
||||
def test_load_data_1min_timeframe(ohlcv_history, mocker, caplog, testdatadir) -> None:
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=ohlcv_history)
|
||||
mocker.patch(f'{EXMS}.get_historic_ohlcv', return_value=ohlcv_history)
|
||||
file = testdatadir / 'UNITTEST_BTC-1m.json'
|
||||
load_data(datadir=testdatadir, timeframe='1m', pairs=['UNITTEST/BTC'])
|
||||
assert file.is_file()
|
||||
@@ -77,12 +77,12 @@ def test_load_data_1min_timeframe(ohlcv_history, mocker, caplog, testdatadir) ->
|
||||
|
||||
|
||||
def test_load_data_mark(ohlcv_history, mocker, caplog, testdatadir) -> None:
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=ohlcv_history)
|
||||
file = testdatadir / 'futures/UNITTEST_USDT-1h-mark.json'
|
||||
mocker.patch(f'{EXMS}.get_historic_ohlcv', return_value=ohlcv_history)
|
||||
file = testdatadir / 'futures/UNITTEST_USDT_USDT-1h-mark.json'
|
||||
load_data(datadir=testdatadir, timeframe='1h', pairs=['UNITTEST/BTC'], candle_type='mark')
|
||||
assert file.is_file()
|
||||
assert not log_has(
|
||||
'Download history data for pair: "UNITTEST/USDT", interval: 1m '
|
||||
'Download history data for pair: "UNITTEST/USDT:USDT", interval: 1m '
|
||||
'and store in None.', caplog
|
||||
)
|
||||
|
||||
@@ -109,7 +109,7 @@ def test_load_data_with_new_pair_1min(ohlcv_history_list, mocker, caplog,
|
||||
Test load_pair_history() with 1 min timeframe
|
||||
"""
|
||||
tmpdir1 = Path(tmpdir)
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=ohlcv_history_list)
|
||||
mocker.patch(f'{EXMS}.get_historic_ohlcv', return_value=ohlcv_history_list)
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
file = tmpdir1 / 'MEME_BTC-1m.json'
|
||||
|
||||
@@ -191,7 +191,7 @@ def test_load_cached_data_for_updating(mocker, testdatadir) -> None:
|
||||
|
||||
test_data = None
|
||||
test_filename = testdatadir.joinpath('UNITTEST_BTC-1m.json')
|
||||
with open(test_filename, "rt") as file:
|
||||
with test_filename.open("rt") as file:
|
||||
test_data = json.load(file)
|
||||
|
||||
test_data_df = ohlcv_to_dataframe(test_data, '1m', 'UNITTEST/BTC',
|
||||
@@ -277,7 +277,7 @@ def test_download_pair_history(
|
||||
subdir,
|
||||
file_tail
|
||||
) -> None:
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=ohlcv_history_list)
|
||||
mocker.patch(f'{EXMS}.get_historic_ohlcv', return_value=ohlcv_history_list)
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
tmpdir1 = Path(tmpdir)
|
||||
file1_1 = tmpdir1 / f'{subdir}MEME_BTC-1m{file_tail}.json'
|
||||
@@ -328,7 +328,7 @@ def test_download_pair_history2(mocker, default_conf, testdatadir) -> None:
|
||||
json_dump_mock = mocker.patch(
|
||||
'freqtrade.data.history.jsondatahandler.JsonDataHandler.ohlcv_store',
|
||||
return_value=None)
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=tick)
|
||||
mocker.patch(f'{EXMS}.get_historic_ohlcv', return_value=tick)
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
_download_pair_history(datadir=testdatadir, exchange=exchange, pair="UNITTEST/BTC",
|
||||
timeframe='1m', candle_type='spot')
|
||||
@@ -340,7 +340,7 @@ def test_download_pair_history2(mocker, default_conf, testdatadir) -> None:
|
||||
|
||||
|
||||
def test_download_backtesting_data_exception(mocker, caplog, default_conf, tmpdir) -> None:
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv',
|
||||
mocker.patch(f'{EXMS}.get_historic_ohlcv',
|
||||
side_effect=Exception('File Error'))
|
||||
tmpdir1 = Path(tmpdir)
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
@@ -506,9 +506,7 @@ def test_refresh_backtest_ohlcv_data(
|
||||
mocker, default_conf, markets, caplog, testdatadir, trademode, callcount):
|
||||
dl_mock = mocker.patch('freqtrade.data.history.history_utils._download_pair_history',
|
||||
MagicMock())
|
||||
mocker.patch(
|
||||
'freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets)
|
||||
)
|
||||
mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets))
|
||||
mocker.patch.object(Path, "exists", MagicMock(return_value=True))
|
||||
mocker.patch.object(Path, "unlink", MagicMock())
|
||||
|
||||
@@ -531,9 +529,7 @@ def test_download_data_no_markets(mocker, default_conf, caplog, testdatadir):
|
||||
MagicMock())
|
||||
|
||||
ex = get_patched_exchange(mocker, default_conf)
|
||||
mocker.patch(
|
||||
'freqtrade.exchange.Exchange.markets', PropertyMock(return_value={})
|
||||
)
|
||||
mocker.patch(f'{EXMS}.markets', PropertyMock(return_value={}))
|
||||
timerange = TimeRange.parse_timerange("20190101-20190102")
|
||||
unav_pairs = refresh_backtest_ohlcv_data(exchange=ex, pairs=["BTT/BTC", "LTC/USDT"],
|
||||
timeframes=["1m", "5m"],
|
||||
@@ -551,9 +547,7 @@ def test_download_data_no_markets(mocker, default_conf, caplog, testdatadir):
|
||||
def test_refresh_backtest_trades_data(mocker, default_conf, markets, caplog, testdatadir):
|
||||
dl_mock = mocker.patch('freqtrade.data.history.history_utils._download_trades_history',
|
||||
MagicMock())
|
||||
mocker.patch(
|
||||
'freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets)
|
||||
)
|
||||
mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets))
|
||||
mocker.patch.object(Path, "exists", MagicMock(return_value=True))
|
||||
mocker.patch.object(Path, "unlink", MagicMock())
|
||||
|
||||
@@ -577,8 +571,7 @@ def test_download_trades_history(trades_history, mocker, default_conf, testdatad
|
||||
tmpdir) -> None:
|
||||
tmpdir1 = Path(tmpdir)
|
||||
ght_mock = MagicMock(side_effect=lambda pair, *args, **kwargs: (pair, trades_history))
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_historic_trades',
|
||||
ght_mock)
|
||||
mocker.patch(f'{EXMS}.get_historic_trades', ght_mock)
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
file1 = tmpdir1 / 'ETH_BTC-trades.json.gz'
|
||||
data_handler = get_datahandler(tmpdir1, data_format='jsongz')
|
||||
@@ -604,8 +597,7 @@ def test_download_trades_history(trades_history, mocker, default_conf, testdatad
|
||||
|
||||
file1.unlink()
|
||||
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_historic_trades',
|
||||
MagicMock(side_effect=ValueError))
|
||||
mocker.patch(f'{EXMS}.get_historic_trades', MagicMock(side_effect=ValueError))
|
||||
|
||||
assert not _download_trades_history(data_handler=data_handler, exchange=exchange,
|
||||
pair='ETH/BTC')
|
||||
@@ -615,8 +607,7 @@ def test_download_trades_history(trades_history, mocker, default_conf, testdatad
|
||||
copyfile(testdatadir / file2.name, file2)
|
||||
|
||||
ght_mock.reset_mock()
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_historic_trades',
|
||||
ght_mock)
|
||||
mocker.patch(f'{EXMS}.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)
|
||||
|
@@ -14,7 +14,7 @@ from freqtrade.data.converter import ohlcv_to_dataframe
|
||||
from freqtrade.edge import Edge, PairInfo
|
||||
from freqtrade.enums import ExitType
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from tests.conftest import get_patched_freqtradebot, log_has
|
||||
from tests.conftest import EXMS, get_patched_freqtradebot, log_has
|
||||
from tests.optimize import (BTContainer, BTrade, _build_backtest_dataframe,
|
||||
_get_frame_time_from_offset)
|
||||
|
||||
@@ -139,7 +139,7 @@ def test_adjust(mocker, edge_conf):
|
||||
assert (edge.adjust(pairs) == ['E/F', 'C/D'])
|
||||
|
||||
|
||||
def test_stoploss(mocker, edge_conf):
|
||||
def test_edge_get_stoploss(mocker, edge_conf):
|
||||
freqtrade = get_patched_freqtradebot(mocker, edge_conf)
|
||||
edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy)
|
||||
mocker.patch('freqtrade.edge.Edge._cached_pairs', mocker.PropertyMock(
|
||||
@@ -150,10 +150,10 @@ def test_stoploss(mocker, edge_conf):
|
||||
}
|
||||
))
|
||||
|
||||
assert edge.stoploss('E/F') == -0.01
|
||||
assert edge.get_stoploss('E/F') == -0.01
|
||||
|
||||
|
||||
def test_nonexisting_stoploss(mocker, edge_conf):
|
||||
def test_nonexisting_get_stoploss(mocker, edge_conf):
|
||||
freqtrade = get_patched_freqtradebot(mocker, edge_conf)
|
||||
edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy)
|
||||
mocker.patch('freqtrade.edge.Edge._cached_pairs', mocker.PropertyMock(
|
||||
@@ -162,7 +162,7 @@ def test_nonexisting_stoploss(mocker, edge_conf):
|
||||
}
|
||||
))
|
||||
|
||||
assert edge.stoploss('N/O') == -0.1
|
||||
assert edge.get_stoploss('N/O') == -0.1
|
||||
|
||||
|
||||
def test_edge_stake_amount(mocker, edge_conf):
|
||||
@@ -261,7 +261,7 @@ def mocked_load_data(datadir, pairs=[], timeframe='0m',
|
||||
|
||||
def test_edge_process_downloaded_data(mocker, edge_conf):
|
||||
freqtrade = get_patched_freqtradebot(mocker, edge_conf)
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.001))
|
||||
mocker.patch(f'{EXMS}.get_fee', MagicMock(return_value=0.001))
|
||||
mocker.patch('freqtrade.edge.edge_positioning.refresh_data', MagicMock())
|
||||
mocker.patch('freqtrade.edge.edge_positioning.load_data', mocked_load_data)
|
||||
edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy)
|
||||
@@ -273,7 +273,7 @@ def test_edge_process_downloaded_data(mocker, edge_conf):
|
||||
|
||||
def test_edge_process_no_data(mocker, edge_conf, caplog):
|
||||
freqtrade = get_patched_freqtradebot(mocker, edge_conf)
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.001))
|
||||
mocker.patch(f'{EXMS}.get_fee', MagicMock(return_value=0.001))
|
||||
mocker.patch('freqtrade.edge.edge_positioning.refresh_data', MagicMock())
|
||||
mocker.patch('freqtrade.edge.edge_positioning.load_data', MagicMock(return_value={}))
|
||||
edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy)
|
||||
@@ -286,7 +286,7 @@ def test_edge_process_no_data(mocker, edge_conf, caplog):
|
||||
|
||||
def test_edge_process_no_trades(mocker, edge_conf, caplog):
|
||||
freqtrade = get_patched_freqtradebot(mocker, edge_conf)
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', return_value=0.001)
|
||||
mocker.patch(f'{EXMS}.get_fee', return_value=0.001)
|
||||
mocker.patch('freqtrade.edge.edge_positioning.refresh_data', )
|
||||
mocker.patch('freqtrade.edge.edge_positioning.load_data', mocked_load_data)
|
||||
# Return empty
|
||||
@@ -303,7 +303,7 @@ def test_edge_process_no_pairs(mocker, edge_conf, caplog):
|
||||
mocker.patch('freqtrade.freqtradebot.validate_config_consistency')
|
||||
|
||||
freqtrade = get_patched_freqtradebot(mocker, edge_conf)
|
||||
fee_mock = mocker.patch('freqtrade.exchange.Exchange.get_fee', return_value=0.001)
|
||||
fee_mock = mocker.patch(f'{EXMS}.get_fee', return_value=0.001)
|
||||
mocker.patch('freqtrade.edge.edge_positioning.refresh_data')
|
||||
mocker.patch('freqtrade.edge.edge_positioning.load_data', mocked_load_data)
|
||||
# Return empty
|
||||
@@ -319,7 +319,7 @@ def test_edge_process_no_pairs(mocker, edge_conf, caplog):
|
||||
|
||||
def test_edge_init_error(mocker, edge_conf,):
|
||||
edge_conf['stake_amount'] = 0.5
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.001))
|
||||
mocker.patch(f'{EXMS}.get_fee', MagicMock(return_value=0.001))
|
||||
with pytest.raises(OperationalException, match='Edge works only with unlimited stake amount'):
|
||||
get_patched_freqtradebot(mocker, edge_conf)
|
||||
|
||||
|
@@ -7,7 +7,7 @@ import pytest
|
||||
|
||||
from freqtrade.enums import CandleType, MarginMode, TradingMode
|
||||
from freqtrade.exceptions import DependencyException, InvalidOrderException, OperationalException
|
||||
from tests.conftest import get_mock_coro, get_patched_exchange, log_has_re
|
||||
from tests.conftest import EXMS, get_mock_coro, get_patched_exchange, log_has_re
|
||||
from tests.exchange.test_exchange import ccxt_exceptionhandlers
|
||||
|
||||
|
||||
@@ -20,10 +20,10 @@ from tests.exchange.test_exchange import ccxt_exceptionhandlers
|
||||
(0.99, 220 * 1.01, "buy"),
|
||||
(0.98, 220 * 1.02, "buy"),
|
||||
])
|
||||
def test_stoploss_order_binance(default_conf, mocker, limitratio, expected, side, trademode):
|
||||
def test_create_stoploss_order_binance(default_conf, mocker, limitratio, expected, side, trademode):
|
||||
api_mock = MagicMock()
|
||||
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
|
||||
order_type = 'stop_loss_limit' if trademode == TradingMode.SPOT else 'limit'
|
||||
order_type = 'stop_loss_limit' if trademode == TradingMode.SPOT else 'stop'
|
||||
|
||||
api_mock.create_order = MagicMock(return_value={
|
||||
'id': order_id,
|
||||
@@ -34,13 +34,13 @@ def test_stoploss_order_binance(default_conf, mocker, limitratio, expected, side
|
||||
default_conf['dry_run'] = False
|
||||
default_conf['margin_mode'] = MarginMode.ISOLATED
|
||||
default_conf['trading_mode'] = trademode
|
||||
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)
|
||||
mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y)
|
||||
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y)
|
||||
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
|
||||
|
||||
with pytest.raises(OperationalException):
|
||||
order = exchange.stoploss(
|
||||
order = exchange.create_stoploss(
|
||||
pair='ETH/BTC',
|
||||
amount=1,
|
||||
stop_price=190,
|
||||
@@ -50,11 +50,11 @@ def test_stoploss_order_binance(default_conf, mocker, limitratio, expected, side
|
||||
)
|
||||
|
||||
api_mock.create_order.reset_mock()
|
||||
order_types = {'stoploss': 'limit'}
|
||||
order_types = {'stoploss': 'limit', 'stoploss_price_type': 'mark'}
|
||||
if limitratio is not None:
|
||||
order_types.update({'stoploss_on_exchange_limit_ratio': limitratio})
|
||||
|
||||
order = exchange.stoploss(
|
||||
order = exchange.create_stoploss(
|
||||
pair='ETH/BTC',
|
||||
amount=1,
|
||||
stop_price=220,
|
||||
@@ -75,14 +75,14 @@ def test_stoploss_order_binance(default_conf, mocker, limitratio, expected, side
|
||||
if trademode == TradingMode.SPOT:
|
||||
params_dict = {'stopPrice': 220}
|
||||
else:
|
||||
params_dict = {'stopPrice': 220, 'reduceOnly': True}
|
||||
params_dict = {'stopPrice': 220, 'reduceOnly': True, 'workingType': 'MARK_PRICE'}
|
||||
assert api_mock.create_order.call_args_list[0][1]['params'] == params_dict
|
||||
|
||||
# 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, 'binance')
|
||||
exchange.stoploss(
|
||||
exchange.create_stoploss(
|
||||
pair='ETH/BTC',
|
||||
amount=1,
|
||||
stop_price=220,
|
||||
@@ -94,7 +94,7 @@ def test_stoploss_order_binance(default_conf, mocker, limitratio, expected, side
|
||||
api_mock.create_order = MagicMock(
|
||||
side_effect=ccxt.InvalidOrder("binance Order would trigger immediately."))
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
|
||||
exchange.stoploss(
|
||||
exchange.create_stoploss(
|
||||
pair='ETH/BTC',
|
||||
amount=1,
|
||||
stop_price=220,
|
||||
@@ -104,22 +104,22 @@ def test_stoploss_order_binance(default_conf, mocker, limitratio, expected, side
|
||||
)
|
||||
|
||||
ccxt_exceptionhandlers(mocker, default_conf, api_mock, "binance",
|
||||
"stoploss", "create_order", retries=1,
|
||||
"create_stoploss", "create_order", retries=1,
|
||||
pair='ETH/BTC', amount=1, stop_price=220, order_types={},
|
||||
side=side, leverage=1.0)
|
||||
|
||||
|
||||
def test_stoploss_order_dry_run_binance(default_conf, mocker):
|
||||
def test_create_stoploss_order_dry_run_binance(default_conf, mocker):
|
||||
api_mock = MagicMock()
|
||||
order_type = 'stop_loss_limit'
|
||||
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)
|
||||
mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y)
|
||||
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y)
|
||||
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
|
||||
|
||||
with pytest.raises(OperationalException):
|
||||
order = exchange.stoploss(
|
||||
order = exchange.create_stoploss(
|
||||
pair='ETH/BTC',
|
||||
amount=1,
|
||||
stop_price=190,
|
||||
@@ -130,7 +130,7 @@ def test_stoploss_order_dry_run_binance(default_conf, mocker):
|
||||
|
||||
api_mock.create_order.reset_mock()
|
||||
|
||||
order = exchange.stoploss(
|
||||
order = exchange.create_stoploss(
|
||||
pair='ETH/BTC',
|
||||
amount=1,
|
||||
stop_price=220,
|
||||
@@ -495,7 +495,8 @@ def test_fill_leverage_tiers_binance_dryrun(default_conf, mocker, leverage_tiers
|
||||
for key, value in leverage_tiers.items():
|
||||
v = exchange._leverage_tiers[key]
|
||||
assert isinstance(v, list)
|
||||
assert len(v) == len(value)
|
||||
# Assert if conftest leverage tiers have less or equal tiers than the exchange
|
||||
assert len(v) >= len(value)
|
||||
|
||||
|
||||
def test_additional_exchange_init_binance(default_conf, mocker):
|
||||
@@ -522,8 +523,15 @@ def test__set_leverage_binance(mocker, default_conf):
|
||||
api_mock.set_leverage = MagicMock()
|
||||
type(api_mock).has = PropertyMock(return_value={'setLeverage': True})
|
||||
default_conf['dry_run'] = False
|
||||
exchange = get_patched_exchange(mocker, default_conf, id="binance")
|
||||
exchange._set_leverage(3.0, trading_mode=TradingMode.MARGIN)
|
||||
default_conf['trading_mode'] = TradingMode.FUTURES
|
||||
default_conf['margin_mode'] = MarginMode.ISOLATED
|
||||
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance")
|
||||
exchange._set_leverage(3.2, 'BTC/USDT:USDT')
|
||||
assert api_mock.set_leverage.call_count == 1
|
||||
# Leverage is rounded to 3.
|
||||
assert api_mock.set_leverage.call_args_list[0][1]['leverage'] == 3
|
||||
assert api_mock.set_leverage.call_args_list[0][1]['symbol'] == 'BTC/USDT:USDT'
|
||||
|
||||
ccxt_exceptionhandlers(
|
||||
mocker,
|
||||
@@ -557,7 +565,7 @@ async def test__async_get_historic_ohlcv_binance(default_conf, mocker, caplog, c
|
||||
exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv)
|
||||
|
||||
pair = 'ETH/BTC'
|
||||
respair, restf, restype, res = await exchange._async_get_historic_ohlcv(
|
||||
respair, restf, restype, res, _ = await exchange._async_get_historic_ohlcv(
|
||||
pair, "5m", 1500000000000, is_new_pair=False, candle_type=candle_type)
|
||||
assert respair == pair
|
||||
assert restf == '5m'
|
||||
@@ -566,7 +574,7 @@ async def test__async_get_historic_ohlcv_binance(default_conf, mocker, caplog, c
|
||||
assert exchange._api_async.fetch_ohlcv.call_count > 400
|
||||
# assert res == ohlcv
|
||||
exchange._api_async.fetch_ohlcv.reset_mock()
|
||||
_, _, _, res = await exchange._async_get_historic_ohlcv(
|
||||
_, _, _, res, _ = await exchange._async_get_historic_ohlcv(
|
||||
pair, "5m", 1500000000000, is_new_pair=True, candle_type=candle_type)
|
||||
|
||||
# Called twice - one "init" call - and one to get the actual data.
|
||||
@@ -575,25 +583,13 @@ async def test__async_get_historic_ohlcv_binance(default_conf, mocker, caplog, c
|
||||
assert log_has_re(r"Candle-data for ETH/BTC available starting with .*", caplog)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("trading_mode,margin_mode,config", [
|
||||
("spot", "", {}),
|
||||
("margin", "cross", {"options": {"defaultType": "margin"}}),
|
||||
("futures", "isolated", {"options": {"defaultType": "future"}}),
|
||||
])
|
||||
def test__ccxt_config(default_conf, mocker, trading_mode, margin_mode, config):
|
||||
default_conf['trading_mode'] = trading_mode
|
||||
default_conf['margin_mode'] = margin_mode
|
||||
exchange = get_patched_exchange(mocker, default_conf, id="binance")
|
||||
assert exchange._ccxt_config == config
|
||||
|
||||
|
||||
@pytest.mark.parametrize('pair,nominal_value,mm_ratio,amt', [
|
||||
("BNB/BUSD", 0.0, 0.025, 0),
|
||||
("BNB/USDT", 100.0, 0.0065, 0),
|
||||
("BTC/USDT", 170.30, 0.004, 0),
|
||||
("BNB/BUSD", 999999.9, 0.1, 27500.0),
|
||||
("BNB/USDT", 5000000.0, 0.15, 233035.0),
|
||||
("BTC/USDT", 600000000, 0.5, 1.997038E8),
|
||||
("BNB/BUSD:BUSD", 0.0, 0.025, 0),
|
||||
("BNB/USDT:USDT", 100.0, 0.0065, 0),
|
||||
("BTC/USDT:USDT", 170.30, 0.004, 0),
|
||||
("BNB/BUSD:BUSD", 999999.9, 0.1, 27500.0),
|
||||
("BNB/USDT:USDT", 5000000.0, 0.15, 233035.0),
|
||||
("BTC/USDT:USDT", 600000000, 0.5, 1.997038E8),
|
||||
])
|
||||
def test_get_maintenance_ratio_and_amt_binance(
|
||||
default_conf,
|
||||
@@ -604,7 +600,7 @@ def test_get_maintenance_ratio_and_amt_binance(
|
||||
mm_ratio,
|
||||
amt,
|
||||
):
|
||||
mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True)
|
||||
mocker.patch(f'{EXMS}.exchange_has', return_value=True)
|
||||
exchange = get_patched_exchange(mocker, default_conf, id="binance")
|
||||
exchange._leverage_tiers = leverage_tiers
|
||||
(result_ratio, result_amt) = exchange.get_maintenance_ratio_and_amt(pair, nominal_value)
|
||||
|
@@ -1,7 +1,7 @@
|
||||
from datetime import datetime
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from tests.conftest import get_patched_exchange
|
||||
from tests.conftest import EXMS, get_patched_exchange
|
||||
|
||||
|
||||
def test_get_trades_for_order(default_conf, mocker):
|
||||
@@ -9,7 +9,7 @@ def test_get_trades_for_order(default_conf, mocker):
|
||||
order_id = 'ABCD-ABCD'
|
||||
since = datetime(2018, 5, 5, 0, 0, 0)
|
||||
default_conf["dry_run"] = False
|
||||
mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True)
|
||||
mocker.patch(f'{EXMS}.exchange_has', return_value=True)
|
||||
api_mock = MagicMock()
|
||||
|
||||
api_mock.fetch_my_trades = MagicMock(return_value=[{'id': 'TTR67E-3PFBD-76IISV',
|
||||
|
74
tests/exchange/test_bybit.py
Normal file
74
tests/exchange/test_bybit.py
Normal file
@@ -0,0 +1,74 @@
|
||||
from datetime import datetime, timezone
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from freqtrade.enums.marginmode import MarginMode
|
||||
from freqtrade.enums.tradingmode import TradingMode
|
||||
from freqtrade.exchange.exchange_utils import timeframe_to_msecs
|
||||
from tests.conftest import get_mock_coro, get_patched_exchange
|
||||
from tests.exchange.test_exchange import ccxt_exceptionhandlers
|
||||
|
||||
|
||||
def test_additional_exchange_init_bybit(default_conf, mocker):
|
||||
default_conf['dry_run'] = False
|
||||
default_conf['trading_mode'] = TradingMode.FUTURES
|
||||
default_conf['margin_mode'] = MarginMode.ISOLATED
|
||||
api_mock = MagicMock()
|
||||
api_mock.set_position_mode = MagicMock(return_value={"dualSidePosition": False})
|
||||
get_patched_exchange(mocker, default_conf, id="bybit", api_mock=api_mock)
|
||||
assert api_mock.set_position_mode.call_count == 1
|
||||
ccxt_exceptionhandlers(mocker, default_conf, api_mock, 'bybit',
|
||||
"additional_exchange_init", "set_position_mode")
|
||||
|
||||
|
||||
async def test_bybit_fetch_funding_rate(default_conf, mocker):
|
||||
default_conf['trading_mode'] = 'futures'
|
||||
default_conf['margin_mode'] = 'isolated'
|
||||
api_mock = MagicMock()
|
||||
api_mock.fetch_funding_rate_history = get_mock_coro(return_value=[])
|
||||
exchange = get_patched_exchange(mocker, default_conf, id='bybit', api_mock=api_mock)
|
||||
limit = 200
|
||||
# Test fetch_funding_rate_history (current data)
|
||||
await exchange._fetch_funding_rate_history(
|
||||
pair='BTC/USDT:USDT',
|
||||
timeframe='4h',
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
assert api_mock.fetch_funding_rate_history.call_count == 1
|
||||
assert api_mock.fetch_funding_rate_history.call_args_list[0][0][0] == 'BTC/USDT:USDT'
|
||||
kwargs = api_mock.fetch_funding_rate_history.call_args_list[0][1]
|
||||
assert kwargs['params'] == {}
|
||||
assert kwargs['since'] is None
|
||||
|
||||
api_mock.fetch_funding_rate_history.reset_mock()
|
||||
since_ms = 1610000000000
|
||||
since_ms_end = since_ms + (timeframe_to_msecs('4h') * limit)
|
||||
# Test fetch_funding_rate_history (current data)
|
||||
await exchange._fetch_funding_rate_history(
|
||||
pair='BTC/USDT:USDT',
|
||||
timeframe='4h',
|
||||
limit=limit,
|
||||
since_ms=since_ms,
|
||||
)
|
||||
|
||||
assert api_mock.fetch_funding_rate_history.call_count == 1
|
||||
assert api_mock.fetch_funding_rate_history.call_args_list[0][0][0] == 'BTC/USDT:USDT'
|
||||
kwargs = api_mock.fetch_funding_rate_history.call_args_list[0][1]
|
||||
assert kwargs['params'] == {'until': since_ms_end}
|
||||
assert kwargs['since'] == since_ms
|
||||
|
||||
|
||||
def test_bybit_get_funding_fees(default_conf, mocker):
|
||||
now = datetime.now(timezone.utc)
|
||||
exchange = get_patched_exchange(mocker, default_conf, id='bybit')
|
||||
exchange._fetch_and_calculate_funding_fees = MagicMock()
|
||||
exchange.get_funding_fees('BTC/USDT:USDT', 1, False, now)
|
||||
assert exchange._fetch_and_calculate_funding_fees.call_count == 0
|
||||
|
||||
default_conf['trading_mode'] = 'futures'
|
||||
default_conf['margin_mode'] = 'isolated'
|
||||
exchange = get_patched_exchange(mocker, default_conf, id='bybit')
|
||||
exchange._fetch_and_calculate_funding_fees = MagicMock()
|
||||
exchange.get_funding_fees('BTC/USDT:USDT', 1, False, now)
|
||||
|
||||
assert exchange._fetch_and_calculate_funding_fees.call_count == 1
|
@@ -8,16 +8,20 @@ suitable to run with freqtrade.
|
||||
from copy import deepcopy
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
from typing import Tuple
|
||||
|
||||
import pytest
|
||||
|
||||
from freqtrade.constants import Config
|
||||
from freqtrade.enums import CandleType
|
||||
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_prev_date
|
||||
from freqtrade.exchange.exchange import timeframe_to_msecs
|
||||
from freqtrade.exchange.exchange import Exchange, timeframe_to_msecs
|
||||
from freqtrade.resolvers.exchange_resolver import ExchangeResolver
|
||||
from tests.conftest import get_default_conf_usdt
|
||||
from tests.conftest import EXMS, get_default_conf_usdt
|
||||
|
||||
|
||||
EXCHANGE_FIXTURE_TYPE = Tuple[Exchange, str]
|
||||
|
||||
# Exchanges that should be tested
|
||||
EXCHANGES = {
|
||||
'bittrex': {
|
||||
@@ -28,15 +32,61 @@ EXCHANGES = {
|
||||
'leverage_tiers_public': False,
|
||||
'leverage_in_spot_market': False,
|
||||
},
|
||||
# 'binance': {
|
||||
# 'pair': 'BTC/USDT',
|
||||
# 'stake_currency': 'USDT',
|
||||
# 'hasQuoteVolume': True,
|
||||
# 'timeframe': '5m',
|
||||
# 'futures': True,
|
||||
# 'leverage_tiers_public': False,
|
||||
# 'leverage_in_spot_market': False,
|
||||
# },
|
||||
'binance': {
|
||||
'pair': 'BTC/USDT',
|
||||
'stake_currency': 'USDT',
|
||||
'use_ci_proxy': True,
|
||||
'hasQuoteVolume': True,
|
||||
'timeframe': '5m',
|
||||
'futures': True,
|
||||
'futures_pair': 'BTC/USDT:USDT',
|
||||
'hasQuoteVolumeFutures': True,
|
||||
'leverage_tiers_public': False,
|
||||
'leverage_in_spot_market': False,
|
||||
'sample_order': [{
|
||||
"symbol": "SOLUSDT",
|
||||
"orderId": 3551312894,
|
||||
"orderListId": -1,
|
||||
"clientOrderId": "x-R4DD3S8297c73a11ccb9dc8f2811ba",
|
||||
"transactTime": 1674493798550,
|
||||
"price": "15.50000000",
|
||||
"origQty": "1.10000000",
|
||||
"executedQty": "0.00000000",
|
||||
"cummulativeQuoteQty": "0.00000000",
|
||||
"status": "NEW",
|
||||
"timeInForce": "GTC",
|
||||
"type": "LIMIT",
|
||||
"side": "BUY",
|
||||
"workingTime": 1674493798550,
|
||||
"fills": [],
|
||||
"selfTradePreventionMode": "NONE",
|
||||
}]
|
||||
},
|
||||
'binanceus': {
|
||||
'pair': 'BTC/USDT',
|
||||
'stake_currency': 'USDT',
|
||||
'hasQuoteVolume': True,
|
||||
'timeframe': '5m',
|
||||
'futures': False,
|
||||
'sample_order': [{
|
||||
"symbol": "SOLUSDT",
|
||||
"orderId": 3551312894,
|
||||
"orderListId": -1,
|
||||
"clientOrderId": "x-R4DD3S8297c73a11ccb9dc8f2811ba",
|
||||
"transactTime": 1674493798550,
|
||||
"price": "15.50000000",
|
||||
"origQty": "1.10000000",
|
||||
"executedQty": "0.00000000",
|
||||
"cummulativeQuoteQty": "0.00000000",
|
||||
"status": "NEW",
|
||||
"timeInForce": "GTC",
|
||||
"type": "LIMIT",
|
||||
"side": "BUY",
|
||||
"workingTime": 1674493798550,
|
||||
"fills": [],
|
||||
"selfTradePreventionMode": "NONE",
|
||||
}]
|
||||
},
|
||||
'kraken': {
|
||||
'pair': 'BTC/USDT',
|
||||
'stake_currency': 'USDT',
|
||||
@@ -52,18 +102,127 @@ EXCHANGES = {
|
||||
'timeframe': '5m',
|
||||
'leverage_tiers_public': False,
|
||||
'leverage_in_spot_market': True,
|
||||
'sample_order': [
|
||||
{'id': '63d6742d0adc5570001d2bbf7'}, # create order
|
||||
{
|
||||
'id': '63d6742d0adc5570001d2bbf7',
|
||||
'symbol': 'SOL-USDT',
|
||||
'opType': 'DEAL',
|
||||
'type': 'limit',
|
||||
'side': 'buy',
|
||||
'price': '15.5',
|
||||
'size': '1.1',
|
||||
'funds': '0',
|
||||
'dealFunds': '17.05',
|
||||
'dealSize': '1.1',
|
||||
'fee': '0.000065252',
|
||||
'feeCurrency': 'USDT',
|
||||
'stp': '',
|
||||
'stop': '',
|
||||
'stopTriggered': False,
|
||||
'stopPrice': '0',
|
||||
'timeInForce': 'GTC',
|
||||
'postOnly': False,
|
||||
'hidden': False,
|
||||
'iceberg': False,
|
||||
'visibleSize': '0',
|
||||
'cancelAfter': 0,
|
||||
'channel': 'API',
|
||||
'clientOid': '0a053870-11bf-41e5-be61-b272a4cb62e1',
|
||||
'remark': None,
|
||||
'tags': 'partner:ccxt',
|
||||
'isActive': False,
|
||||
'cancelExist': False,
|
||||
'createdAt': 1674493798550,
|
||||
'tradeType': 'TRADE'
|
||||
}],
|
||||
},
|
||||
'gateio': {
|
||||
'gate': {
|
||||
'pair': 'BTC/USDT',
|
||||
'stake_currency': 'USDT',
|
||||
'hasQuoteVolume': True,
|
||||
'timeframe': '5m',
|
||||
'futures': True,
|
||||
'futures_pair': 'BTC/USDT:USDT',
|
||||
'hasQuoteVolumeFutures': True,
|
||||
'leverage_tiers_public': True,
|
||||
'leverage_in_spot_market': True,
|
||||
'sample_order': [
|
||||
{
|
||||
"id": "276266139423",
|
||||
"text": "apiv4",
|
||||
"create_time": "1674493798",
|
||||
"update_time": "1674493798",
|
||||
"create_time_ms": "1674493798550",
|
||||
"update_time_ms": "1674493798550",
|
||||
"status": "closed",
|
||||
"currency_pair": "SOL_USDT",
|
||||
"type": "limit",
|
||||
"account": "spot",
|
||||
"side": "buy",
|
||||
"amount": "1.1",
|
||||
"price": "15.5",
|
||||
"time_in_force": "gtc",
|
||||
"iceberg": "0",
|
||||
"left": "0",
|
||||
"fill_price": "17.05",
|
||||
"filled_total": "17.05",
|
||||
"avg_deal_price": "15.5",
|
||||
"fee": "0.0000018",
|
||||
"fee_currency": "SOL",
|
||||
"point_fee": "0",
|
||||
"gt_fee": "0",
|
||||
"gt_maker_fee": "0",
|
||||
"gt_taker_fee": "0.0015",
|
||||
"gt_discount": True,
|
||||
"rebated_fee": "0",
|
||||
"rebated_fee_currency": "USDT"
|
||||
},
|
||||
{
|
||||
# market order
|
||||
'id': '276401180529',
|
||||
'text': 'apiv4',
|
||||
'create_time': '1674493798',
|
||||
'update_time': '1674493798',
|
||||
'create_time_ms': '1674493798550',
|
||||
'update_time_ms': '1674493798550',
|
||||
'status': 'cancelled',
|
||||
'currency_pair': 'SOL_USDT',
|
||||
'type': 'market',
|
||||
'account': 'spot',
|
||||
'side': 'buy',
|
||||
'amount': '17.05',
|
||||
'price': '0',
|
||||
'time_in_force': 'ioc',
|
||||
'iceberg': '0',
|
||||
'left': '0.0000000016228',
|
||||
'fill_price': '17.05',
|
||||
'filled_total': '17.05',
|
||||
'avg_deal_price': '15.5',
|
||||
'fee': '0',
|
||||
'fee_currency': 'SOL',
|
||||
'point_fee': '0.0199999999967544',
|
||||
'gt_fee': '0',
|
||||
'gt_maker_fee': '0',
|
||||
'gt_taker_fee': '0',
|
||||
'gt_discount': False,
|
||||
'rebated_fee': '0',
|
||||
'rebated_fee_currency': 'USDT'
|
||||
}
|
||||
],
|
||||
},
|
||||
'okx': {
|
||||
'pair': 'BTC/USDT',
|
||||
'stake_currency': 'USDT',
|
||||
'hasQuoteVolume': True,
|
||||
'timeframe': '5m',
|
||||
'futures': True,
|
||||
'futures_pair': 'BTC/USDT:USDT',
|
||||
'hasQuoteVolumeFutures': False,
|
||||
'leverage_tiers_public': True,
|
||||
'leverage_in_spot_market': True,
|
||||
},
|
||||
'bybit': {
|
||||
'pair': 'BTC/USDT',
|
||||
'stake_currency': 'USDT',
|
||||
'hasQuoteVolume': True,
|
||||
@@ -72,10 +231,27 @@ EXCHANGES = {
|
||||
'futures': True,
|
||||
'leverage_tiers_public': True,
|
||||
'leverage_in_spot_market': True,
|
||||
'sample_order': [
|
||||
{
|
||||
"orderId": "1274754916287346280",
|
||||
"orderLinkId": "1666798627015730",
|
||||
"symbol": "SOLUSDT",
|
||||
"createTime": "1674493798550",
|
||||
"orderPrice": "15.5",
|
||||
"orderQty": "1.1",
|
||||
"orderType": "LIMIT",
|
||||
"side": "BUY",
|
||||
"status": "NEW",
|
||||
"timeInForce": "GTC",
|
||||
"accountId": "5555555",
|
||||
"execQty": "0",
|
||||
"orderCategory": "0"
|
||||
}
|
||||
]
|
||||
},
|
||||
'huobi': {
|
||||
'pair': 'BTC/USDT',
|
||||
'stake_currency': 'USDT',
|
||||
'pair': 'ETH/BTC',
|
||||
'stake_currency': 'BTC',
|
||||
'hasQuoteVolume': True,
|
||||
'timeframe': '5m',
|
||||
'futures': False,
|
||||
@@ -103,8 +279,27 @@ def exchange_conf():
|
||||
return config
|
||||
|
||||
|
||||
def set_test_proxy(config: Config, use_proxy: bool) -> Config:
|
||||
# Set proxy to test in CI.
|
||||
import os
|
||||
if use_proxy and (proxy := os.environ.get('CI_WEB_PROXY')):
|
||||
config1 = deepcopy(config)
|
||||
config1['exchange']['ccxt_config'] = {
|
||||
"aiohttp_proxy": proxy,
|
||||
'proxies': {
|
||||
'https': proxy,
|
||||
'http': proxy,
|
||||
}
|
||||
}
|
||||
return config1
|
||||
|
||||
return config
|
||||
|
||||
|
||||
@pytest.fixture(params=EXCHANGES, scope="class")
|
||||
def exchange(request, exchange_conf):
|
||||
exchange_conf = set_test_proxy(
|
||||
exchange_conf, EXCHANGES[request.param].get('use_ci_proxy', False))
|
||||
exchange_conf['exchange']['name'] = request.param
|
||||
exchange_conf['stake_currency'] = EXCHANGES[request.param]['stake_currency']
|
||||
exchange = ExchangeResolver.load_exchange(request.param, exchange_conf, validate=True)
|
||||
@@ -114,9 +309,11 @@ def exchange(request, exchange_conf):
|
||||
|
||||
@pytest.fixture(params=EXCHANGES, scope="class")
|
||||
def exchange_futures(request, exchange_conf, class_mocker):
|
||||
if not EXCHANGES[request.param].get('futures') is True:
|
||||
if EXCHANGES[request.param].get('futures') is not True:
|
||||
yield None, request.param
|
||||
else:
|
||||
exchange_conf = set_test_proxy(
|
||||
exchange_conf, EXCHANGES[request.param].get('use_ci_proxy', False))
|
||||
exchange_conf = deepcopy(exchange_conf)
|
||||
exchange_conf['exchange']['name'] = request.param
|
||||
exchange_conf['trading_mode'] = 'futures'
|
||||
@@ -125,12 +322,12 @@ def exchange_futures(request, exchange_conf, class_mocker):
|
||||
|
||||
class_mocker.patch(
|
||||
'freqtrade.exchange.binance.Binance.fill_leverage_tiers')
|
||||
class_mocker.patch('freqtrade.exchange.exchange.Exchange.fetch_trading_fees')
|
||||
class_mocker.patch(f'{EXMS}.fetch_trading_fees')
|
||||
class_mocker.patch('freqtrade.exchange.okx.Okx.additional_exchange_init')
|
||||
class_mocker.patch('freqtrade.exchange.binance.Binance.additional_exchange_init')
|
||||
class_mocker.patch('freqtrade.exchange.exchange.Exchange.load_cached_leverage_tiers',
|
||||
return_value=None)
|
||||
class_mocker.patch('freqtrade.exchange.exchange.Exchange.cache_leverage_tiers')
|
||||
class_mocker.patch('freqtrade.exchange.bybit.Bybit.additional_exchange_init')
|
||||
class_mocker.patch(f'{EXMS}.load_cached_leverage_tiers', return_value=None)
|
||||
class_mocker.patch(f'{EXMS}.cache_leverage_tiers')
|
||||
|
||||
exchange = ExchangeResolver.load_exchange(
|
||||
request.param, exchange_conf, validate=True, load_leverage_tiers=True)
|
||||
@@ -141,34 +338,34 @@ def exchange_futures(request, exchange_conf, class_mocker):
|
||||
@pytest.mark.longrun
|
||||
class TestCCXTExchange():
|
||||
|
||||
def test_load_markets(self, exchange):
|
||||
exchange, exchangename = exchange
|
||||
def test_load_markets(self, exchange: EXCHANGE_FIXTURE_TYPE):
|
||||
exch, exchangename = exchange
|
||||
pair = EXCHANGES[exchangename]['pair']
|
||||
markets = exchange.markets
|
||||
markets = exch.markets
|
||||
assert pair in markets
|
||||
assert isinstance(markets[pair], dict)
|
||||
assert exchange.market_is_spot(markets[pair])
|
||||
assert exch.market_is_spot(markets[pair])
|
||||
|
||||
def test_has_validations(self, exchange):
|
||||
def test_has_validations(self, exchange: EXCHANGE_FIXTURE_TYPE):
|
||||
|
||||
exchange, exchangename = exchange
|
||||
exch, exchangename = exchange
|
||||
|
||||
exchange.validate_ordertypes({
|
||||
exch.validate_ordertypes({
|
||||
'entry': 'limit',
|
||||
'exit': 'limit',
|
||||
'stoploss': 'limit',
|
||||
})
|
||||
|
||||
if exchangename == 'gateio':
|
||||
# gateio doesn't have market orders on spot
|
||||
if exchangename == 'gate':
|
||||
# gate doesn't have market orders on spot
|
||||
return
|
||||
exchange.validate_ordertypes({
|
||||
exch.validate_ordertypes({
|
||||
'entry': 'market',
|
||||
'exit': 'market',
|
||||
'stoploss': 'market',
|
||||
})
|
||||
|
||||
def test_load_markets_futures(self, exchange_futures):
|
||||
def test_load_markets_futures(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
|
||||
exchange, exchangename = exchange_futures
|
||||
if not exchange:
|
||||
# exchange_futures only returns values for supported exchanges
|
||||
@@ -181,11 +378,37 @@ class TestCCXTExchange():
|
||||
|
||||
assert exchange.market_is_future(markets[pair])
|
||||
|
||||
def test_ccxt_fetch_tickers(self, exchange):
|
||||
exchange, exchangename = exchange
|
||||
def test_ccxt_order_parse(self, exchange: EXCHANGE_FIXTURE_TYPE):
|
||||
exch, exchange_name = exchange
|
||||
if orders := EXCHANGES[exchange_name].get('sample_order'):
|
||||
for order in orders:
|
||||
po = exch._api.parse_order(order)
|
||||
assert isinstance(po['id'], str)
|
||||
assert po['id'] is not None
|
||||
if len(order.keys()) < 5:
|
||||
# Kucoin case
|
||||
assert po['status'] == 'closed'
|
||||
continue
|
||||
assert po['timestamp'] == 1674493798550
|
||||
assert isinstance(po['datetime'], str)
|
||||
assert isinstance(po['timestamp'], int)
|
||||
assert isinstance(po['price'], float)
|
||||
assert po['price'] == 15.5
|
||||
if po['average'] is not None:
|
||||
assert isinstance(po['average'], float)
|
||||
assert po['average'] == 15.5
|
||||
assert po['symbol'] == 'SOL/USDT'
|
||||
assert isinstance(po['amount'], float)
|
||||
assert po['amount'] == 1.1
|
||||
assert isinstance(po['status'], str)
|
||||
else:
|
||||
pytest.skip(f"No sample order available for exchange {exchange_name}")
|
||||
|
||||
def test_ccxt_fetch_tickers(self, exchange: EXCHANGE_FIXTURE_TYPE):
|
||||
exch, exchangename = exchange
|
||||
pair = EXCHANGES[exchangename]['pair']
|
||||
|
||||
tickers = exchange.get_tickers()
|
||||
tickers = exch.get_tickers()
|
||||
assert pair in tickers
|
||||
assert 'ask' in tickers[pair]
|
||||
assert tickers[pair]['ask'] is not None
|
||||
@@ -195,11 +418,30 @@ class TestCCXTExchange():
|
||||
if EXCHANGES[exchangename].get('hasQuoteVolume'):
|
||||
assert tickers[pair]['quoteVolume'] is not None
|
||||
|
||||
def test_ccxt_fetch_ticker(self, exchange):
|
||||
exchange, exchangename = exchange
|
||||
def test_ccxt_fetch_tickers_futures(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
|
||||
exch, exchangename = exchange_futures
|
||||
if not exch or exchangename in ('gate'):
|
||||
# exchange_futures only returns values for supported exchanges
|
||||
return
|
||||
|
||||
pair = EXCHANGES[exchangename]['pair']
|
||||
pair = EXCHANGES[exchangename].get('futures_pair', pair)
|
||||
|
||||
tickers = exch.get_tickers()
|
||||
assert pair in tickers
|
||||
assert 'ask' in tickers[pair]
|
||||
assert tickers[pair]['ask'] is not None
|
||||
assert 'bid' in tickers[pair]
|
||||
assert tickers[pair]['bid'] is not None
|
||||
assert 'quoteVolume' in tickers[pair]
|
||||
if EXCHANGES[exchangename].get('hasQuoteVolumeFutures'):
|
||||
assert tickers[pair]['quoteVolume'] is not None
|
||||
|
||||
def test_ccxt_fetch_ticker(self, exchange: EXCHANGE_FIXTURE_TYPE):
|
||||
exch, exchangename = exchange
|
||||
pair = EXCHANGES[exchangename]['pair']
|
||||
|
||||
ticker = exchange.fetch_ticker(pair)
|
||||
ticker = exch.fetch_ticker(pair)
|
||||
assert 'ask' in ticker
|
||||
assert ticker['ask'] is not None
|
||||
assert 'bid' in ticker
|
||||
@@ -208,21 +450,23 @@ class TestCCXTExchange():
|
||||
if EXCHANGES[exchangename].get('hasQuoteVolume'):
|
||||
assert ticker['quoteVolume'] is not None
|
||||
|
||||
def test_ccxt_fetch_l2_orderbook(self, exchange):
|
||||
exchange, exchangename = exchange
|
||||
def test_ccxt_fetch_l2_orderbook(self, exchange: EXCHANGE_FIXTURE_TYPE):
|
||||
exch, exchangename = exchange
|
||||
pair = EXCHANGES[exchangename]['pair']
|
||||
l2 = exchange.fetch_l2_order_book(pair)
|
||||
l2 = exch.fetch_l2_order_book(pair)
|
||||
assert 'asks' in l2
|
||||
assert 'bids' in l2
|
||||
assert len(l2['asks']) >= 1
|
||||
assert len(l2['bids']) >= 1
|
||||
l2_limit_range = exchange._ft_has['l2_limit_range']
|
||||
l2_limit_range_required = exchange._ft_has['l2_limit_range_required']
|
||||
if exchangename == 'gateio':
|
||||
# TODO: Gateio is unstable here at the moment, ignoring the limit partially.
|
||||
l2_limit_range = exch._ft_has['l2_limit_range']
|
||||
l2_limit_range_required = exch._ft_has['l2_limit_range_required']
|
||||
if exchangename == 'gate':
|
||||
# TODO: Gate is unstable here at the moment, ignoring the limit partially.
|
||||
return
|
||||
for val in [1, 2, 5, 25, 100]:
|
||||
l2 = exchange.fetch_l2_order_book(pair, val)
|
||||
for val in [1, 2, 5, 25, 50, 100]:
|
||||
if val > 50 and exchangename == 'bybit':
|
||||
continue
|
||||
l2 = exch.fetch_l2_order_book(pair, val)
|
||||
if not l2_limit_range or val in l2_limit_range:
|
||||
if val > 50:
|
||||
# Orderbooks are not always this deep.
|
||||
@@ -232,7 +476,7 @@ class TestCCXTExchange():
|
||||
assert len(l2['asks']) == val
|
||||
assert len(l2['bids']) == val
|
||||
else:
|
||||
next_limit = exchange.get_next_limit_in_list(
|
||||
next_limit = exch.get_next_limit_in_list(
|
||||
val, l2_limit_range, l2_limit_range_required)
|
||||
if next_limit is None:
|
||||
assert len(l2['asks']) > 100
|
||||
@@ -245,23 +489,23 @@ class TestCCXTExchange():
|
||||
assert len(l2['asks']) == next_limit
|
||||
assert len(l2['asks']) == next_limit
|
||||
|
||||
def test_ccxt_fetch_ohlcv(self, exchange):
|
||||
exchange, exchangename = exchange
|
||||
def test_ccxt_fetch_ohlcv(self, exchange: EXCHANGE_FIXTURE_TYPE):
|
||||
exch, exchangename = exchange
|
||||
pair = EXCHANGES[exchangename]['pair']
|
||||
timeframe = EXCHANGES[exchangename]['timeframe']
|
||||
|
||||
pair_tf = (pair, timeframe, CandleType.SPOT)
|
||||
|
||||
ohlcv = exchange.refresh_latest_ohlcv([pair_tf])
|
||||
ohlcv = exch.refresh_latest_ohlcv([pair_tf])
|
||||
assert isinstance(ohlcv, dict)
|
||||
assert len(ohlcv[pair_tf]) == len(exchange.klines(pair_tf))
|
||||
# assert len(exchange.klines(pair_tf)) > 200
|
||||
assert len(ohlcv[pair_tf]) == len(exch.klines(pair_tf))
|
||||
# assert len(exch.klines(pair_tf)) > 200
|
||||
# Assume 90% uptime ...
|
||||
assert len(exchange.klines(pair_tf)) > exchange.ohlcv_candle_limit(
|
||||
assert len(exch.klines(pair_tf)) > exch.ohlcv_candle_limit(
|
||||
timeframe, CandleType.SPOT) * 0.90
|
||||
# Check if last-timeframe is within the last 2 intervals
|
||||
now = datetime.now(timezone.utc) - timedelta(minutes=(timeframe_to_minutes(timeframe) * 2))
|
||||
assert exchange.klines(pair_tf).iloc[-1]['date'] >= timeframe_to_prev_date(timeframe, now)
|
||||
assert exch.klines(pair_tf).iloc[-1]['date'] >= timeframe_to_prev_date(timeframe, now)
|
||||
|
||||
def ccxt__async_get_candle_history(self, exchange, exchangename, pair, timeframe, candle_type):
|
||||
|
||||
@@ -289,17 +533,20 @@ class TestCCXTExchange():
|
||||
assert len(candles) >= min(candle_count, candle_count1)
|
||||
assert candles[0][0] == since_ms or (since_ms + timeframe_ms)
|
||||
|
||||
def test_ccxt__async_get_candle_history(self, exchange):
|
||||
exchange, exchangename = exchange
|
||||
# For some weired reason, this test returns random lengths for bittrex.
|
||||
if not exchange._ft_has['ohlcv_has_history'] or exchangename in ('bittrex'):
|
||||
return
|
||||
def test_ccxt__async_get_candle_history(self, exchange: EXCHANGE_FIXTURE_TYPE):
|
||||
exc, exchangename = exchange
|
||||
if exchangename in ('bittrex'):
|
||||
# For some weired reason, this test returns random lengths for bittrex.
|
||||
pytest.skip("Exchange doesn't provide stable ohlcv history")
|
||||
|
||||
if not exc._ft_has['ohlcv_has_history']:
|
||||
pytest.skip("Exchange does not support candle history")
|
||||
pair = EXCHANGES[exchangename]['pair']
|
||||
timeframe = EXCHANGES[exchangename]['timeframe']
|
||||
self.ccxt__async_get_candle_history(
|
||||
exchange, exchangename, pair, timeframe, CandleType.SPOT)
|
||||
exc, exchangename, pair, timeframe, CandleType.SPOT)
|
||||
|
||||
def test_ccxt__async_get_candle_history_futures(self, exchange_futures):
|
||||
def test_ccxt__async_get_candle_history_futures(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
|
||||
exchange, exchangename = exchange_futures
|
||||
if not exchange:
|
||||
# exchange_futures only returns values for supported exchanges
|
||||
@@ -309,7 +556,7 @@ class TestCCXTExchange():
|
||||
self.ccxt__async_get_candle_history(
|
||||
exchange, exchangename, pair, timeframe, CandleType.FUTURES)
|
||||
|
||||
def test_ccxt_fetch_funding_rate_history(self, exchange_futures):
|
||||
def test_ccxt_fetch_funding_rate_history(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
|
||||
exchange, exchangename = exchange_futures
|
||||
if not exchange:
|
||||
# exchange_futures only returns values for supported exchanges
|
||||
@@ -347,7 +594,7 @@ class TestCCXTExchange():
|
||||
(rate['open'].min() != rate['open'].max())
|
||||
)
|
||||
|
||||
def test_ccxt_fetch_mark_price_history(self, exchange_futures):
|
||||
def test_ccxt_fetch_mark_price_history(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
|
||||
exchange, exchangename = exchange_futures
|
||||
if not exchange:
|
||||
# exchange_futures only returns values for supported exchanges
|
||||
@@ -371,7 +618,7 @@ class TestCCXTExchange():
|
||||
assert mark_candles[mark_candles['date'] == prev_hour].iloc[0]['open'] != 0.0
|
||||
assert mark_candles[mark_candles['date'] == this_hour].iloc[0]['open'] != 0.0
|
||||
|
||||
def test_ccxt__calculate_funding_fees(self, exchange_futures):
|
||||
def test_ccxt__calculate_funding_fees(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
|
||||
exchange, exchangename = exchange_futures
|
||||
if not exchange:
|
||||
# exchange_futures only returns values for supported exchanges
|
||||
@@ -387,16 +634,16 @@ class TestCCXTExchange():
|
||||
|
||||
# TODO: tests fetch_trades (?)
|
||||
|
||||
def test_ccxt_get_fee(self, exchange):
|
||||
exchange, exchangename = exchange
|
||||
def test_ccxt_get_fee(self, exchange: EXCHANGE_FIXTURE_TYPE):
|
||||
exch, exchangename = exchange
|
||||
pair = EXCHANGES[exchangename]['pair']
|
||||
threshold = 0.01
|
||||
assert 0 < exchange.get_fee(pair, 'limit', 'buy') < threshold
|
||||
assert 0 < exchange.get_fee(pair, 'limit', 'sell') < threshold
|
||||
assert 0 < exchange.get_fee(pair, 'market', 'buy') < threshold
|
||||
assert 0 < exchange.get_fee(pair, 'market', 'sell') < threshold
|
||||
assert 0 < exch.get_fee(pair, 'limit', 'buy') < threshold
|
||||
assert 0 < exch.get_fee(pair, 'limit', 'sell') < threshold
|
||||
assert 0 < exch.get_fee(pair, 'market', 'buy') < threshold
|
||||
assert 0 < exch.get_fee(pair, 'market', 'sell') < threshold
|
||||
|
||||
def test_ccxt_get_max_leverage_spot(self, exchange):
|
||||
def test_ccxt_get_max_leverage_spot(self, exchange: EXCHANGE_FIXTURE_TYPE):
|
||||
spot, spot_name = exchange
|
||||
if spot:
|
||||
leverage_in_market_spot = EXCHANGES[spot_name].get('leverage_in_spot_market')
|
||||
@@ -406,7 +653,7 @@ class TestCCXTExchange():
|
||||
assert (isinstance(spot_leverage, float) or isinstance(spot_leverage, int))
|
||||
assert spot_leverage >= 1.0
|
||||
|
||||
def test_ccxt_get_max_leverage_futures(self, exchange_futures):
|
||||
def test_ccxt_get_max_leverage_futures(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
|
||||
futures, futures_name = exchange_futures
|
||||
if futures:
|
||||
leverage_tiers_public = EXCHANGES[futures_name].get('leverage_tiers_public')
|
||||
@@ -419,7 +666,7 @@ class TestCCXTExchange():
|
||||
assert (isinstance(futures_leverage, float) or isinstance(futures_leverage, int))
|
||||
assert futures_leverage >= 1.0
|
||||
|
||||
def test_ccxt_get_contract_size(self, exchange_futures):
|
||||
def test_ccxt_get_contract_size(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
|
||||
futures, futures_name = exchange_futures
|
||||
if futures:
|
||||
futures_pair = EXCHANGES[futures_name].get(
|
||||
@@ -430,7 +677,7 @@ class TestCCXTExchange():
|
||||
assert (isinstance(contract_size, float) or isinstance(contract_size, int))
|
||||
assert contract_size >= 0.0
|
||||
|
||||
def test_ccxt_load_leverage_tiers(self, exchange_futures):
|
||||
def test_ccxt_load_leverage_tiers(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
|
||||
futures, futures_name = exchange_futures
|
||||
if futures and EXCHANGES[futures_name].get('leverage_tiers_public'):
|
||||
leverage_tiers = futures.load_leverage_tiers()
|
||||
@@ -463,7 +710,7 @@ class TestCCXTExchange():
|
||||
oldminNotional = tier['minNotional']
|
||||
oldmaxNotional = tier['maxNotional']
|
||||
|
||||
def test_ccxt_dry_run_liquidation_price(self, exchange_futures):
|
||||
def test_ccxt_dry_run_liquidation_price(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
|
||||
futures, futures_name = exchange_futures
|
||||
if futures and EXCHANGES[futures_name].get('leverage_tiers_public'):
|
||||
|
||||
@@ -473,28 +720,30 @@ class TestCCXTExchange():
|
||||
)
|
||||
|
||||
liquidation_price = futures.dry_run_liquidation_price(
|
||||
futures_pair,
|
||||
40000,
|
||||
False,
|
||||
100,
|
||||
100,
|
||||
100,
|
||||
pair=futures_pair,
|
||||
open_rate=40000,
|
||||
is_short=False,
|
||||
amount=100,
|
||||
stake_amount=100,
|
||||
leverage=5,
|
||||
wallet_balance=100,
|
||||
)
|
||||
assert (isinstance(liquidation_price, float))
|
||||
assert liquidation_price >= 0.0
|
||||
|
||||
liquidation_price = futures.dry_run_liquidation_price(
|
||||
futures_pair,
|
||||
40000,
|
||||
False,
|
||||
100,
|
||||
100,
|
||||
100,
|
||||
pair=futures_pair,
|
||||
open_rate=40000,
|
||||
is_short=False,
|
||||
amount=100,
|
||||
stake_amount=100,
|
||||
leverage=5,
|
||||
wallet_balance=100,
|
||||
)
|
||||
assert (isinstance(liquidation_price, float))
|
||||
assert liquidation_price >= 0.0
|
||||
|
||||
def test_ccxt_get_max_pair_stake_amount(self, exchange_futures):
|
||||
def test_ccxt_get_max_pair_stake_amount(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
|
||||
futures, futures_name = exchange_futures
|
||||
if futures:
|
||||
futures_pair = EXCHANGES[futures_name].get(
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -5,22 +5,22 @@ import pytest
|
||||
|
||||
from freqtrade.enums import MarginMode, TradingMode
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange import Gateio
|
||||
from freqtrade.exchange import Gate
|
||||
from freqtrade.resolvers.exchange_resolver import ExchangeResolver
|
||||
from tests.conftest import get_patched_exchange
|
||||
from tests.conftest import EXMS, get_patched_exchange
|
||||
|
||||
|
||||
def test_validate_order_types_gateio(default_conf, mocker):
|
||||
default_conf['exchange']['name'] = 'gateio'
|
||||
mocker.patch('freqtrade.exchange.Exchange._init_ccxt')
|
||||
mocker.patch('freqtrade.exchange.Exchange._load_markets', return_value={})
|
||||
mocker.patch('freqtrade.exchange.Exchange.validate_pairs')
|
||||
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes')
|
||||
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
|
||||
mocker.patch('freqtrade.exchange.Exchange.validate_pricing')
|
||||
mocker.patch('freqtrade.exchange.Exchange.name', 'Bittrex')
|
||||
exch = ExchangeResolver.load_exchange('gateio', default_conf, True)
|
||||
assert isinstance(exch, Gateio)
|
||||
def test_validate_order_types_gate(default_conf, mocker):
|
||||
default_conf['exchange']['name'] = 'gate'
|
||||
mocker.patch(f'{EXMS}._init_ccxt')
|
||||
mocker.patch(f'{EXMS}._load_markets', return_value={})
|
||||
mocker.patch(f'{EXMS}.validate_pairs')
|
||||
mocker.patch(f'{EXMS}.validate_timeframes')
|
||||
mocker.patch(f'{EXMS}.validate_stakecurrency')
|
||||
mocker.patch(f'{EXMS}.validate_pricing')
|
||||
mocker.patch(f'{EXMS}.name', 'Gate')
|
||||
exch = ExchangeResolver.load_exchange('gate', default_conf, True)
|
||||
assert isinstance(exch, Gate)
|
||||
|
||||
default_conf['order_types'] = {
|
||||
'entry': 'market',
|
||||
@@ -31,18 +31,18 @@ def test_validate_order_types_gateio(default_conf, mocker):
|
||||
|
||||
with pytest.raises(OperationalException,
|
||||
match=r'Exchange .* does not support market orders.'):
|
||||
ExchangeResolver.load_exchange('gateio', default_conf, True)
|
||||
ExchangeResolver.load_exchange('gate', default_conf, True)
|
||||
|
||||
# market-orders supported on futures markets.
|
||||
default_conf['trading_mode'] = 'futures'
|
||||
default_conf['margin_mode'] = 'isolated'
|
||||
ex = ExchangeResolver.load_exchange('gateio', default_conf, True)
|
||||
ex = ExchangeResolver.load_exchange('gate', default_conf, True)
|
||||
assert ex
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_persistence")
|
||||
def test_fetch_stoploss_order_gateio(default_conf, mocker):
|
||||
exchange = get_patched_exchange(mocker, default_conf, id='gateio')
|
||||
def test_fetch_stoploss_order_gate(default_conf, mocker):
|
||||
exchange = get_patched_exchange(mocker, default_conf, id='gate')
|
||||
|
||||
fetch_order_mock = MagicMock()
|
||||
exchange.fetch_order = fetch_order_mock
|
||||
@@ -56,7 +56,7 @@ def test_fetch_stoploss_order_gateio(default_conf, mocker):
|
||||
default_conf['trading_mode'] = 'futures'
|
||||
default_conf['margin_mode'] = 'isolated'
|
||||
|
||||
exchange = get_patched_exchange(mocker, default_conf, id='gateio')
|
||||
exchange = get_patched_exchange(mocker, default_conf, id='gate')
|
||||
|
||||
exchange.fetch_order = MagicMock(return_value={
|
||||
'status': 'closed',
|
||||
@@ -73,8 +73,8 @@ def test_fetch_stoploss_order_gateio(default_conf, mocker):
|
||||
assert exchange.fetch_order.call_args_list[1][1]['order_id'] == '222555'
|
||||
|
||||
|
||||
def test_cancel_stoploss_order_gateio(default_conf, mocker):
|
||||
exchange = get_patched_exchange(mocker, default_conf, id='gateio')
|
||||
def test_cancel_stoploss_order_gate(default_conf, mocker):
|
||||
exchange = get_patched_exchange(mocker, default_conf, id='gate')
|
||||
|
||||
cancel_order_mock = MagicMock()
|
||||
exchange.cancel_order = cancel_order_mock
|
||||
@@ -90,8 +90,8 @@ def test_cancel_stoploss_order_gateio(default_conf, mocker):
|
||||
(1501, 1499, 1501, "sell"),
|
||||
(1499, 1501, 1499, "buy")
|
||||
])
|
||||
def test_stoploss_adjust_gateio(mocker, default_conf, sl1, sl2, sl3, side):
|
||||
exchange = get_patched_exchange(mocker, default_conf, id='gateio')
|
||||
def test_stoploss_adjust_gate(mocker, default_conf, sl1, sl2, sl3, side):
|
||||
exchange = get_patched_exchange(mocker, default_conf, id='gate')
|
||||
order = {
|
||||
'price': 1500,
|
||||
'stopPrice': 1500,
|
||||
@@ -104,8 +104,8 @@ def test_stoploss_adjust_gateio(mocker, default_conf, sl1, sl2, sl3, side):
|
||||
('taker', 0.0005, 0.0001554325),
|
||||
('maker', 0.0, 0.0),
|
||||
])
|
||||
def test_fetch_my_trades_gateio(mocker, default_conf, takerormaker, rate, cost):
|
||||
mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True)
|
||||
def test_fetch_my_trades_gate(mocker, default_conf, takerormaker, rate, cost):
|
||||
mocker.patch(f'{EXMS}.exchange_has', return_value=True)
|
||||
tick = {'ETH/USDT:USDT': {
|
||||
'info': {'user_id': '',
|
||||
'taker_fee': '0.0018',
|
||||
@@ -134,7 +134,7 @@ def test_fetch_my_trades_gateio(mocker, default_conf, takerormaker, rate, cost):
|
||||
'takerOrMaker': takerormaker,
|
||||
'amount': 1, # 1 contract
|
||||
}])
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock=api_mock, id='gateio')
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock=api_mock, id='gate')
|
||||
exchange._trading_fees = tick
|
||||
trades = exchange.get_trades_for_order('22255', 'ETH/USDT:USDT', datetime.now(timezone.utc))
|
||||
trade = trades[0]
|
@@ -5,7 +5,7 @@ import ccxt
|
||||
import pytest
|
||||
|
||||
from freqtrade.exceptions import DependencyException, InvalidOrderException, OperationalException
|
||||
from tests.conftest import get_patched_exchange
|
||||
from tests.conftest import EXMS, get_patched_exchange
|
||||
from tests.exchange.test_exchange import ccxt_exceptionhandlers
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ from tests.exchange.test_exchange import ccxt_exceptionhandlers
|
||||
(0.99, 220 * 0.99, "sell"),
|
||||
(0.98, 220 * 0.98, "sell"),
|
||||
])
|
||||
def test_stoploss_order_huobi(default_conf, mocker, limitratio, expected, side):
|
||||
def test_create_stoploss_order_huobi(default_conf, mocker, limitratio, expected, side):
|
||||
api_mock = MagicMock()
|
||||
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
|
||||
order_type = 'stop-limit'
|
||||
@@ -26,21 +26,21 @@ def test_stoploss_order_huobi(default_conf, mocker, limitratio, expected, side):
|
||||
}
|
||||
})
|
||||
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)
|
||||
mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y)
|
||||
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y)
|
||||
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'huobi')
|
||||
|
||||
with pytest.raises(OperationalException):
|
||||
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190,
|
||||
order_types={'stoploss_on_exchange_limit_ratio': 1.05},
|
||||
side=side,
|
||||
leverage=1.0)
|
||||
order = exchange.create_stoploss(pair='ETH/BTC', amount=1, stop_price=190,
|
||||
order_types={'stoploss_on_exchange_limit_ratio': 1.05},
|
||||
side=side,
|
||||
leverage=1.0)
|
||||
|
||||
api_mock.create_order.reset_mock()
|
||||
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,
|
||||
side=side, leverage=1.0)
|
||||
order = exchange.create_stoploss(
|
||||
pair='ETH/BTC', amount=1, stop_price=220, order_types=order_types, side=side, leverage=1.0)
|
||||
|
||||
assert 'id' in order
|
||||
assert 'info' in order
|
||||
@@ -59,40 +59,40 @@ def test_stoploss_order_huobi(default_conf, mocker, limitratio, expected, side):
|
||||
with pytest.raises(DependencyException):
|
||||
api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance"))
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'huobi')
|
||||
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220,
|
||||
order_types={}, side=side, leverage=1.0)
|
||||
exchange.create_stoploss(pair='ETH/BTC', amount=1, stop_price=220,
|
||||
order_types={}, side=side, leverage=1.0)
|
||||
|
||||
with pytest.raises(InvalidOrderException):
|
||||
api_mock.create_order = MagicMock(
|
||||
side_effect=ccxt.InvalidOrder("binance Order would trigger immediately."))
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
|
||||
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220,
|
||||
order_types={}, side=side, leverage=1.0)
|
||||
exchange.create_stoploss(pair='ETH/BTC', amount=1, stop_price=220,
|
||||
order_types={}, side=side, leverage=1.0)
|
||||
|
||||
ccxt_exceptionhandlers(mocker, default_conf, api_mock, "huobi",
|
||||
"stoploss", "create_order", retries=1,
|
||||
"create_stoploss", "create_order", retries=1,
|
||||
pair='ETH/BTC', amount=1, stop_price=220, order_types={},
|
||||
side=side, leverage=1.0)
|
||||
|
||||
|
||||
def test_stoploss_order_dry_run_huobi(default_conf, mocker):
|
||||
def test_create_stoploss_order_dry_run_huobi(default_conf, mocker):
|
||||
api_mock = MagicMock()
|
||||
order_type = 'stop-limit'
|
||||
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)
|
||||
mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y)
|
||||
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y)
|
||||
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'huobi')
|
||||
|
||||
with pytest.raises(OperationalException):
|
||||
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190,
|
||||
order_types={'stoploss_on_exchange_limit_ratio': 1.05},
|
||||
side='sell', leverage=1.0)
|
||||
order = exchange.create_stoploss(pair='ETH/BTC', amount=1, stop_price=190,
|
||||
order_types={'stoploss_on_exchange_limit_ratio': 1.05},
|
||||
side='sell', leverage=1.0)
|
||||
|
||||
api_mock.create_order.reset_mock()
|
||||
|
||||
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220,
|
||||
order_types={}, side='sell', leverage=1.0)
|
||||
order = exchange.create_stoploss(pair='ETH/BTC', amount=1, stop_price=220,
|
||||
order_types={}, side='sell', leverage=1.0)
|
||||
|
||||
assert 'id' in order
|
||||
assert 'info' in order
|
||||
|
@@ -5,7 +5,7 @@ import ccxt
|
||||
import pytest
|
||||
|
||||
from freqtrade.exceptions import DependencyException, InvalidOrderException
|
||||
from tests.conftest import get_patched_exchange
|
||||
from tests.conftest import EXMS, get_patched_exchange
|
||||
from tests.exchange.test_exchange import ccxt_exceptionhandlers
|
||||
|
||||
|
||||
@@ -28,8 +28,8 @@ def test_buy_kraken_trading_agreement(default_conf, mocker):
|
||||
})
|
||||
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)
|
||||
mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y)
|
||||
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id="kraken")
|
||||
|
||||
order = exchange.create_order(
|
||||
@@ -68,8 +68,8 @@ def test_sell_kraken_trading_agreement(default_conf, mocker):
|
||||
})
|
||||
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)
|
||||
mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y)
|
||||
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id="kraken")
|
||||
|
||||
order = exchange.create_order(pair='ETH/BTC', ordertype=order_type,
|
||||
@@ -179,7 +179,7 @@ def test_get_balances_prod(default_conf, mocker):
|
||||
("sell", 217.8),
|
||||
("buy", 222.2),
|
||||
])
|
||||
def test_stoploss_order_kraken(default_conf, mocker, ordertype, side, adjustedprice):
|
||||
def test_create_stoploss_order_kraken(default_conf, mocker, ordertype, side, adjustedprice):
|
||||
api_mock = MagicMock()
|
||||
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
|
||||
|
||||
@@ -191,12 +191,12 @@ def test_stoploss_order_kraken(default_conf, mocker, ordertype, side, adjustedpr
|
||||
})
|
||||
|
||||
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)
|
||||
mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y)
|
||||
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y)
|
||||
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken')
|
||||
|
||||
order = exchange.stoploss(
|
||||
order = exchange.create_stoploss(
|
||||
pair='ETH/BTC',
|
||||
amount=1,
|
||||
stop_price=220,
|
||||
@@ -230,7 +230,7 @@ def test_stoploss_order_kraken(default_conf, mocker, ordertype, side, adjustedpr
|
||||
with pytest.raises(DependencyException):
|
||||
api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance"))
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken')
|
||||
exchange.stoploss(
|
||||
exchange.create_stoploss(
|
||||
pair='ETH/BTC',
|
||||
amount=1,
|
||||
stop_price=220,
|
||||
@@ -243,7 +243,7 @@ def test_stoploss_order_kraken(default_conf, mocker, ordertype, side, adjustedpr
|
||||
api_mock.create_order = MagicMock(
|
||||
side_effect=ccxt.InvalidOrder("kraken Order would trigger immediately."))
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken')
|
||||
exchange.stoploss(
|
||||
exchange.create_stoploss(
|
||||
pair='ETH/BTC',
|
||||
amount=1,
|
||||
stop_price=220,
|
||||
@@ -253,23 +253,23 @@ def test_stoploss_order_kraken(default_conf, mocker, ordertype, side, adjustedpr
|
||||
)
|
||||
|
||||
ccxt_exceptionhandlers(mocker, default_conf, api_mock, "kraken",
|
||||
"stoploss", "create_order", retries=1,
|
||||
"create_stoploss", "create_order", retries=1,
|
||||
pair='ETH/BTC', amount=1, stop_price=220, order_types={},
|
||||
side=side, leverage=1.0)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('side', ['buy', 'sell'])
|
||||
def test_stoploss_order_dry_run_kraken(default_conf, mocker, side):
|
||||
def test_create_stoploss_order_dry_run_kraken(default_conf, mocker, side):
|
||||
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)
|
||||
mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y)
|
||||
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y)
|
||||
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken')
|
||||
|
||||
api_mock.create_order.reset_mock()
|
||||
|
||||
order = exchange.stoploss(
|
||||
order = exchange.create_stoploss(
|
||||
pair='ETH/BTC',
|
||||
amount=1,
|
||||
stop_price=220,
|
||||
|
@@ -5,7 +5,7 @@ import ccxt
|
||||
import pytest
|
||||
|
||||
from freqtrade.exceptions import DependencyException, InvalidOrderException, OperationalException
|
||||
from tests.conftest import get_patched_exchange
|
||||
from tests.conftest import EXMS, get_patched_exchange
|
||||
from tests.exchange.test_exchange import ccxt_exceptionhandlers
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ from tests.exchange.test_exchange import ccxt_exceptionhandlers
|
||||
(0.99, 220 * 0.99, "sell"),
|
||||
(0.98, 220 * 0.98, "sell"),
|
||||
])
|
||||
def test_stoploss_order_kucoin(default_conf, mocker, limitratio, expected, side, order_type):
|
||||
def test_create_stoploss_order_kucoin(default_conf, mocker, limitratio, expected, side, order_type):
|
||||
api_mock = MagicMock()
|
||||
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
|
||||
|
||||
@@ -26,24 +26,24 @@ def test_stoploss_order_kucoin(default_conf, mocker, limitratio, expected, side,
|
||||
}
|
||||
})
|
||||
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)
|
||||
mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y)
|
||||
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y)
|
||||
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kucoin')
|
||||
if order_type == 'limit':
|
||||
with pytest.raises(OperationalException):
|
||||
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190,
|
||||
order_types={
|
||||
'stoploss': order_type,
|
||||
'stoploss_on_exchange_limit_ratio': 1.05},
|
||||
side=side, leverage=1.0)
|
||||
order = exchange.create_stoploss(pair='ETH/BTC', amount=1, stop_price=190,
|
||||
order_types={
|
||||
'stoploss': order_type,
|
||||
'stoploss_on_exchange_limit_ratio': 1.05},
|
||||
side=side, leverage=1.0)
|
||||
|
||||
api_mock.create_order.reset_mock()
|
||||
order_types = {'stoploss': order_type}
|
||||
if limitratio is not None:
|
||||
order_types.update({'stoploss_on_exchange_limit_ratio': limitratio})
|
||||
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220,
|
||||
order_types=order_types, side=side, leverage=1.0)
|
||||
order = exchange.create_stoploss(pair='ETH/BTC', amount=1, stop_price=220,
|
||||
order_types=order_types, side=side, leverage=1.0)
|
||||
|
||||
assert 'id' in order
|
||||
assert 'info' in order
|
||||
@@ -67,18 +67,18 @@ def test_stoploss_order_kucoin(default_conf, mocker, limitratio, expected, side,
|
||||
with pytest.raises(DependencyException):
|
||||
api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance"))
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kucoin')
|
||||
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220,
|
||||
order_types={}, side=side, leverage=1.0)
|
||||
exchange.create_stoploss(pair='ETH/BTC', amount=1, stop_price=220,
|
||||
order_types={}, side=side, leverage=1.0)
|
||||
|
||||
with pytest.raises(InvalidOrderException):
|
||||
api_mock.create_order = MagicMock(
|
||||
side_effect=ccxt.InvalidOrder("kucoin Order would trigger immediately."))
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kucoin')
|
||||
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220,
|
||||
order_types={}, side=side, leverage=1.0)
|
||||
exchange.create_stoploss(pair='ETH/BTC', amount=1, stop_price=220,
|
||||
order_types={}, side=side, leverage=1.0)
|
||||
|
||||
ccxt_exceptionhandlers(mocker, default_conf, api_mock, "kucoin",
|
||||
"stoploss", "create_order", retries=1,
|
||||
"create_stoploss", "create_order", retries=1,
|
||||
pair='ETH/BTC', amount=1, stop_price=220, order_types={},
|
||||
side=side, leverage=1.0)
|
||||
|
||||
@@ -87,21 +87,21 @@ def test_stoploss_order_dry_run_kucoin(default_conf, mocker):
|
||||
api_mock = MagicMock()
|
||||
order_type = 'market'
|
||||
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)
|
||||
mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y)
|
||||
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y)
|
||||
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kucoin')
|
||||
|
||||
with pytest.raises(OperationalException):
|
||||
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190,
|
||||
order_types={'stoploss': 'limit',
|
||||
'stoploss_on_exchange_limit_ratio': 1.05},
|
||||
side='sell', leverage=1.0)
|
||||
order = exchange.create_stoploss(pair='ETH/BTC', amount=1, stop_price=190,
|
||||
order_types={'stoploss': 'limit',
|
||||
'stoploss_on_exchange_limit_ratio': 1.05},
|
||||
side='sell', leverage=1.0)
|
||||
|
||||
api_mock.create_order.reset_mock()
|
||||
|
||||
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220,
|
||||
order_types={}, side='sell', leverage=1.0)
|
||||
order = exchange.create_stoploss(pair='ETH/BTC', amount=1, stop_price=220,
|
||||
order_types={}, side='sell', leverage=1.0)
|
||||
|
||||
assert 'id' in order
|
||||
assert 'info' in order
|
||||
@@ -125,3 +125,45 @@ def test_stoploss_adjust_kucoin(mocker, default_conf):
|
||||
# Test with invalid order case
|
||||
order['stopPrice'] = None
|
||||
assert exchange.stoploss_adjust(1501, order, 'sell')
|
||||
|
||||
|
||||
@pytest.mark.parametrize("side", ["buy", "sell"])
|
||||
@pytest.mark.parametrize("ordertype,rate", [
|
||||
("market", None),
|
||||
("market", 200),
|
||||
("limit", 200),
|
||||
("stop_loss_limit", 200)
|
||||
])
|
||||
def test_kucoin_create_order(default_conf, mocker, side, ordertype, rate):
|
||||
api_mock = MagicMock()
|
||||
order_id = 'test_prod_{}_{}'.format(side, randint(0, 10 ** 6))
|
||||
api_mock.create_order = MagicMock(return_value={
|
||||
'id': order_id,
|
||||
'info': {
|
||||
'foo': 'bar'
|
||||
},
|
||||
'symbol': 'XRP/USDT',
|
||||
'amount': 1
|
||||
})
|
||||
default_conf['dry_run'] = False
|
||||
mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y)
|
||||
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id='kucoin')
|
||||
exchange._set_leverage = MagicMock()
|
||||
exchange.set_margin_mode = MagicMock()
|
||||
|
||||
order = exchange.create_order(
|
||||
pair='XRP/USDT',
|
||||
ordertype=ordertype,
|
||||
side=side,
|
||||
amount=1,
|
||||
rate=rate,
|
||||
leverage=1.0
|
||||
)
|
||||
|
||||
assert 'id' in order
|
||||
assert 'info' in order
|
||||
assert order['id'] == order_id
|
||||
assert order['amount'] == 1
|
||||
# Status must be faked to open for kucoin.
|
||||
assert order['status'] == 'open'
|
||||
|
@@ -46,7 +46,7 @@ def test_get_maintenance_ratio_and_amt_okx(
|
||||
default_conf['margin_mode'] = 'isolated'
|
||||
default_conf['dry_run'] = False
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Okx',
|
||||
'freqtrade.exchange.okx.Okx',
|
||||
exchange_has=MagicMock(return_value=True),
|
||||
load_leverage_tiers=MagicMock(return_value={
|
||||
'ETH/USDT:USDT': [
|
||||
@@ -195,12 +195,12 @@ def test_get_max_pair_stake_amount_okx(default_conf, mocker, leverage_tiers):
|
||||
exchange = get_patched_exchange(mocker, default_conf, id="okx")
|
||||
exchange._leverage_tiers = leverage_tiers
|
||||
|
||||
assert exchange.get_max_pair_stake_amount('BNB/BUSD', 1.0) == 30000000
|
||||
assert exchange.get_max_pair_stake_amount('BNB/USDT', 1.0) == 50000000
|
||||
assert exchange.get_max_pair_stake_amount('BTC/USDT', 1.0) == 1000000000
|
||||
assert exchange.get_max_pair_stake_amount('BTC/USDT', 1.0, 10.0) == 100000000
|
||||
assert exchange.get_max_pair_stake_amount('BNB/BUSD:BUSD', 1.0) == 30000000
|
||||
assert exchange.get_max_pair_stake_amount('BNB/USDT:USDT', 1.0) == 50000000
|
||||
assert exchange.get_max_pair_stake_amount('BTC/USDT:USDT', 1.0) == 1000000000
|
||||
assert exchange.get_max_pair_stake_amount('BTC/USDT:USDT', 1.0, 10.0) == 100000000
|
||||
|
||||
assert exchange.get_max_pair_stake_amount('TTT/USDT', 1.0) == float('inf') # Not in tiers
|
||||
assert exchange.get_max_pair_stake_amount('TTT/USDT:USDT', 1.0) == float('inf') # Not in tiers
|
||||
|
||||
|
||||
@pytest.mark.parametrize('mode,side,reduceonly,result', [
|
||||
|
@@ -27,7 +27,7 @@ def freqai_conf(default_conf, tmpdir):
|
||||
"timerange": "20180110-20180115",
|
||||
"freqai": {
|
||||
"enabled": True,
|
||||
"purge_old_models": True,
|
||||
"purge_old_models": 2,
|
||||
"train_period_days": 2,
|
||||
"backtest_period_days": 10,
|
||||
"live_retrain_hours": 0,
|
||||
@@ -46,6 +46,8 @@ def freqai_conf(default_conf, tmpdir):
|
||||
"use_SVM_to_remove_outliers": True,
|
||||
"stratify_training_data": 0,
|
||||
"indicator_periods_candles": [10],
|
||||
"shuffle_after_split": False,
|
||||
"buffer_train_data_candles": 0
|
||||
},
|
||||
"data_split_parameters": {"test_size": 0.33, "shuffle": False},
|
||||
"model_training_parameters": {"n_estimators": 100},
|
||||
@@ -76,7 +78,9 @@ def make_rl_config(conf):
|
||||
"rr": 1,
|
||||
"profit_aim": 0.02,
|
||||
"win_reward_factor": 2
|
||||
}}
|
||||
},
|
||||
"drop_ohlc_from_features": False
|
||||
}
|
||||
|
||||
return conf
|
||||
|
||||
|
@@ -35,8 +35,8 @@ def test_freqai_backtest_start_backtest_list(freqai_conf, mocker, testdatadir, c
|
||||
args = get_args(args)
|
||||
bt_config = setup_optimize_configuration(args, RunMode.BACKTEST)
|
||||
Backtesting(bt_config)
|
||||
assert log_has_re('Using --strategy-list with FreqAI REQUIRES all strategies to have identical '
|
||||
'populate_any_indicators.', caplog)
|
||||
assert log_has_re('Using --strategy-list with FreqAI REQUIRES all strategies to have identical',
|
||||
caplog)
|
||||
Backtesting.cleanup()
|
||||
|
||||
|
||||
|
@@ -82,7 +82,7 @@ def test_compute_distances(mocker, freqai_conf):
|
||||
freqai = make_data_dictionary(mocker, freqai_conf)
|
||||
freqai_conf['freqai']['feature_parameters'].update({"DI_threshold": 1})
|
||||
avg_mean_dist = freqai.dk.compute_distances()
|
||||
assert round(avg_mean_dist, 2) == 1.99
|
||||
assert round(avg_mean_dist, 2) == 1.98
|
||||
|
||||
|
||||
def test_use_SVM_to_remove_outliers_and_outlier_protection(mocker, freqai_conf, caplog):
|
||||
@@ -90,7 +90,7 @@ def test_use_SVM_to_remove_outliers_and_outlier_protection(mocker, freqai_conf,
|
||||
freqai_conf['freqai']['feature_parameters'].update({"outlier_protection_percentage": 0.1})
|
||||
freqai.dk.use_SVM_to_remove_outliers(predict=False)
|
||||
assert log_has_re(
|
||||
"SVM detected 7.36%",
|
||||
"SVM detected 7.83%",
|
||||
caplog,
|
||||
)
|
||||
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import platform
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
@@ -13,10 +14,14 @@ from freqtrade.freqai.utils import download_all_data_for_training, get_required_
|
||||
from freqtrade.optimize.backtesting import Backtesting
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.plugins.pairlistmanager import PairListManager
|
||||
from tests.conftest import create_mock_trades, get_patched_exchange, log_has_re
|
||||
from tests.conftest import EXMS, create_mock_trades, get_patched_exchange, log_has_re
|
||||
from tests.freqai.conftest import get_patched_freqai_strategy, make_rl_config
|
||||
|
||||
|
||||
def is_py11() -> bool:
|
||||
return sys.version_info >= (3, 11)
|
||||
|
||||
|
||||
def is_arm() -> bool:
|
||||
machine = platform.machine()
|
||||
return "arm" in machine or "aarch64" in machine
|
||||
@@ -27,22 +32,32 @@ def is_mac() -> bool:
|
||||
return "Darwin" in machine
|
||||
|
||||
|
||||
@pytest.mark.parametrize('model, pca, dbscan, float32', [
|
||||
('LightGBMRegressor', True, False, True),
|
||||
('XGBoostRegressor', False, True, False),
|
||||
('XGBoostRFRegressor', False, False, False),
|
||||
('CatboostRegressor', False, False, False),
|
||||
('ReinforcementLearner', False, True, False),
|
||||
('ReinforcementLearner_multiproc', False, False, False),
|
||||
('ReinforcementLearner_test_4ac', False, False, False)
|
||||
])
|
||||
def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca, dbscan, float32):
|
||||
if is_arm() and model == 'CatboostRegressor':
|
||||
def can_run_model(model: str) -> None:
|
||||
if (is_arm() or is_py11()) and "Catboost" in model:
|
||||
pytest.skip("CatBoost is not supported on ARM")
|
||||
|
||||
if is_mac() and 'Reinforcement' in model:
|
||||
if is_mac() and not is_arm() and 'Reinforcement' in model:
|
||||
pytest.skip("Reinforcement learning module not available on intel based Mac OS")
|
||||
|
||||
if is_py11() and 'Reinforcement' in model:
|
||||
pytest.skip("Reinforcement learning currently not available on python 3.11.")
|
||||
|
||||
|
||||
@pytest.mark.parametrize('model, pca, dbscan, float32, can_short, shuffle, buffer', [
|
||||
('LightGBMRegressor', True, False, True, True, False, 0),
|
||||
('XGBoostRegressor', False, True, False, True, False, 10),
|
||||
('XGBoostRFRegressor', False, False, False, True, False, 0),
|
||||
('CatboostRegressor', False, False, False, True, True, 0),
|
||||
('ReinforcementLearner', False, True, False, True, False, 0),
|
||||
('ReinforcementLearner_multiproc', False, False, False, True, False, 0),
|
||||
('ReinforcementLearner_test_3ac', False, False, False, False, False, 0),
|
||||
('ReinforcementLearner_test_3ac', False, False, False, True, False, 0),
|
||||
('ReinforcementLearner_test_4ac', False, False, False, True, False, 0)
|
||||
])
|
||||
def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca,
|
||||
dbscan, float32, can_short, shuffle, buffer):
|
||||
|
||||
can_run_model(model)
|
||||
model_save_ext = 'joblib'
|
||||
freqai_conf.update({"freqaimodel": model})
|
||||
freqai_conf.update({"timerange": "20180110-20180130"})
|
||||
@@ -50,6 +65,8 @@ def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca,
|
||||
freqai_conf['freqai']['feature_parameters'].update({"principal_component_analysis": pca})
|
||||
freqai_conf['freqai']['feature_parameters'].update({"use_DBSCAN_to_remove_outliers": dbscan})
|
||||
freqai_conf.update({"reduce_df_footprint": float32})
|
||||
freqai_conf['freqai']['feature_parameters'].update({"shuffle_after_split": shuffle})
|
||||
freqai_conf['freqai']['feature_parameters'].update({"buffer_train_data_candles": buffer})
|
||||
|
||||
if 'ReinforcementLearner' in model:
|
||||
model_save_ext = 'zip'
|
||||
@@ -58,18 +75,9 @@ def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca,
|
||||
freqai_conf['freqai']['feature_parameters'].update({"use_SVM_to_remove_outliers": True})
|
||||
freqai_conf['freqai']['data_split_parameters'].update({'shuffle': True})
|
||||
|
||||
if 'test_4ac' in model:
|
||||
freqai_conf["freqaimodel_path"] = str(Path(__file__).parents[1] / "freqai" / "test_models")
|
||||
|
||||
if 'ReinforcementLearner' in model:
|
||||
model_save_ext = 'zip'
|
||||
freqai_conf = make_rl_config(freqai_conf)
|
||||
# test the RL guardrails
|
||||
freqai_conf['freqai']['feature_parameters'].update({"use_SVM_to_remove_outliers": True})
|
||||
freqai_conf['freqai']['data_split_parameters'].update({'shuffle': True})
|
||||
|
||||
if 'test_4ac' in model:
|
||||
if 'test_3ac' in model or 'test_4ac' in model:
|
||||
freqai_conf["freqaimodel_path"] = str(Path(__file__).parents[1] / "freqai" / "test_models")
|
||||
freqai_conf["freqai"]["rl_config"]["drop_ohlc_from_features"] = True
|
||||
|
||||
strategy = get_patched_freqai_strategy(mocker, freqai_conf)
|
||||
exchange = get_patched_exchange(mocker, freqai_conf)
|
||||
@@ -77,6 +85,7 @@ def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca,
|
||||
strategy.freqai_info = freqai_conf.get("freqai", {})
|
||||
freqai = strategy.freqai
|
||||
freqai.live = True
|
||||
freqai.can_short = can_short
|
||||
freqai.dk = FreqaiDataKitchen(freqai_conf)
|
||||
freqai.dk.set_paths('ADA/BTC', 10000)
|
||||
timerange = TimeRange.parse_timerange("20180110-20180130")
|
||||
@@ -113,7 +122,7 @@ def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca,
|
||||
('CatboostClassifierMultiTarget', "freqai_test_multimodel_classifier_strat")
|
||||
])
|
||||
def test_extract_data_and_train_model_MultiTargets(mocker, freqai_conf, model, strat):
|
||||
if is_arm() and 'Catboost' in model:
|
||||
if (is_arm() or is_py11()) and 'Catboost' in model:
|
||||
pytest.skip("CatBoost is not supported on ARM")
|
||||
|
||||
freqai_conf.update({"timerange": "20180110-20180130"})
|
||||
@@ -155,7 +164,7 @@ def test_extract_data_and_train_model_MultiTargets(mocker, freqai_conf, model, s
|
||||
'XGBoostRFClassifier',
|
||||
])
|
||||
def test_extract_data_and_train_model_Classifiers(mocker, freqai_conf, model):
|
||||
if is_arm() and model == 'CatboostClassifier':
|
||||
if (is_arm() or is_py11()) and model == 'CatboostClassifier':
|
||||
pytest.skip("CatBoost is not supported on ARM")
|
||||
|
||||
freqai_conf.update({"freqaimodel": model})
|
||||
@@ -202,13 +211,11 @@ def test_extract_data_and_train_model_Classifiers(mocker, freqai_conf, model):
|
||||
],
|
||||
)
|
||||
def test_start_backtesting(mocker, freqai_conf, model, num_files, strat, caplog):
|
||||
can_run_model(model)
|
||||
|
||||
freqai_conf.get("freqai", {}).update({"save_backtest_models": True})
|
||||
freqai_conf['runmode'] = RunMode.BACKTEST
|
||||
if is_arm() and "Catboost" in model:
|
||||
pytest.skip("CatBoost is not supported on ARM")
|
||||
|
||||
if is_mac() and 'Reinforcement' in model:
|
||||
pytest.skip("Reinforcement learning module not available on intel based Mac OS")
|
||||
Trade.use_db = False
|
||||
|
||||
freqai_conf.update({"freqaimodel": model})
|
||||
@@ -221,6 +228,9 @@ def test_start_backtesting(mocker, freqai_conf, model, num_files, strat, caplog)
|
||||
if 'test_4ac' in model:
|
||||
freqai_conf["freqaimodel_path"] = str(Path(__file__).parents[1] / "freqai" / "test_models")
|
||||
|
||||
freqai_conf.get("freqai", {}).get("feature_parameters", {}).update(
|
||||
{"indicator_periods_candles": [2]})
|
||||
|
||||
strategy = get_patched_freqai_strategy(mocker, freqai_conf)
|
||||
exchange = get_patched_exchange(mocker, freqai_conf)
|
||||
strategy.dp = DataProvider(freqai_conf, exchange)
|
||||
@@ -231,15 +241,14 @@ def test_start_backtesting(mocker, freqai_conf, model, num_files, strat, caplog)
|
||||
timerange = TimeRange.parse_timerange("20180110-20180130")
|
||||
freqai.dd.load_all_pair_histories(timerange, freqai.dk)
|
||||
sub_timerange = TimeRange.parse_timerange("20180110-20180130")
|
||||
corr_df, base_df = freqai.dd.get_base_and_corr_dataframes(sub_timerange, "LTC/BTC", freqai.dk)
|
||||
_, base_df = freqai.dd.get_base_and_corr_dataframes(sub_timerange, "LTC/BTC", freqai.dk)
|
||||
df = base_df[freqai_conf["timeframe"]]
|
||||
|
||||
df = freqai.dk.use_strategy_to_populate_indicators(strategy, corr_df, base_df, "LTC/BTC")
|
||||
df = freqai.cache_corr_pairlist_dfs(df, freqai.dk)
|
||||
for i in range(5):
|
||||
df[f'%-constant_{i}'] = i
|
||||
|
||||
metadata = {"pair": "LTC/BTC"}
|
||||
freqai.start_backtesting(df, metadata, freqai.dk)
|
||||
freqai.start_backtesting(df, metadata, freqai.dk, strategy)
|
||||
model_folders = [x for x in freqai.dd.full_path.iterdir() if x.is_dir()]
|
||||
|
||||
assert len(model_folders) == num_files
|
||||
@@ -260,6 +269,8 @@ def test_start_backtesting_subdaily_backtest_period(mocker, freqai_conf):
|
||||
freqai_conf.update({"timerange": "20180120-20180124"})
|
||||
freqai_conf.get("freqai", {}).update({"backtest_period_days": 0.5})
|
||||
freqai_conf.get("freqai", {}).update({"save_backtest_models": True})
|
||||
freqai_conf.get("freqai", {}).get("feature_parameters", {}).update(
|
||||
{"indicator_periods_candles": [2]})
|
||||
strategy = get_patched_freqai_strategy(mocker, freqai_conf)
|
||||
exchange = get_patched_exchange(mocker, freqai_conf)
|
||||
strategy.dp = DataProvider(freqai_conf, exchange)
|
||||
@@ -270,12 +281,11 @@ def test_start_backtesting_subdaily_backtest_period(mocker, freqai_conf):
|
||||
timerange = TimeRange.parse_timerange("20180110-20180130")
|
||||
freqai.dd.load_all_pair_histories(timerange, freqai.dk)
|
||||
sub_timerange = TimeRange.parse_timerange("20180110-20180130")
|
||||
corr_df, base_df = freqai.dd.get_base_and_corr_dataframes(sub_timerange, "LTC/BTC", freqai.dk)
|
||||
|
||||
df = freqai.dk.use_strategy_to_populate_indicators(strategy, corr_df, base_df, "LTC/BTC")
|
||||
_, base_df = freqai.dd.get_base_and_corr_dataframes(sub_timerange, "LTC/BTC", freqai.dk)
|
||||
df = base_df[freqai_conf["timeframe"]]
|
||||
|
||||
metadata = {"pair": "LTC/BTC"}
|
||||
freqai.start_backtesting(df, metadata, freqai.dk)
|
||||
freqai.start_backtesting(df, metadata, freqai.dk, strategy)
|
||||
model_folders = [x for x in freqai.dd.full_path.iterdir() if x.is_dir()]
|
||||
|
||||
assert len(model_folders) == 9
|
||||
@@ -286,6 +296,8 @@ def test_start_backtesting_subdaily_backtest_period(mocker, freqai_conf):
|
||||
def test_start_backtesting_from_existing_folder(mocker, freqai_conf, caplog):
|
||||
freqai_conf.update({"timerange": "20180120-20180130"})
|
||||
freqai_conf.get("freqai", {}).update({"save_backtest_models": True})
|
||||
freqai_conf.get("freqai", {}).get("feature_parameters", {}).update(
|
||||
{"indicator_periods_candles": [2]})
|
||||
strategy = get_patched_freqai_strategy(mocker, freqai_conf)
|
||||
exchange = get_patched_exchange(mocker, freqai_conf)
|
||||
strategy.dp = DataProvider(freqai_conf, exchange)
|
||||
@@ -295,15 +307,14 @@ def test_start_backtesting_from_existing_folder(mocker, freqai_conf, caplog):
|
||||
freqai.dk = FreqaiDataKitchen(freqai_conf)
|
||||
timerange = TimeRange.parse_timerange("20180110-20180130")
|
||||
freqai.dd.load_all_pair_histories(timerange, freqai.dk)
|
||||
sub_timerange = TimeRange.parse_timerange("20180110-20180130")
|
||||
corr_df, base_df = freqai.dd.get_base_and_corr_dataframes(sub_timerange, "LTC/BTC", freqai.dk)
|
||||
|
||||
df = freqai.dk.use_strategy_to_populate_indicators(strategy, corr_df, base_df, "LTC/BTC")
|
||||
sub_timerange = TimeRange.parse_timerange("20180101-20180130")
|
||||
_, base_df = freqai.dd.get_base_and_corr_dataframes(sub_timerange, "LTC/BTC", freqai.dk)
|
||||
df = base_df[freqai_conf["timeframe"]]
|
||||
|
||||
pair = "ADA/BTC"
|
||||
metadata = {"pair": pair}
|
||||
freqai.dk.pair = pair
|
||||
freqai.start_backtesting(df, metadata, freqai.dk)
|
||||
freqai.start_backtesting(df, metadata, freqai.dk, strategy)
|
||||
model_folders = [x for x in freqai.dd.full_path.iterdir() if x.is_dir()]
|
||||
|
||||
assert len(model_folders) == 2
|
||||
@@ -321,14 +332,13 @@ def test_start_backtesting_from_existing_folder(mocker, freqai_conf, caplog):
|
||||
timerange = TimeRange.parse_timerange("20180110-20180130")
|
||||
freqai.dd.load_all_pair_histories(timerange, freqai.dk)
|
||||
sub_timerange = TimeRange.parse_timerange("20180110-20180130")
|
||||
corr_df, base_df = freqai.dd.get_base_and_corr_dataframes(sub_timerange, "LTC/BTC", freqai.dk)
|
||||
|
||||
df = freqai.dk.use_strategy_to_populate_indicators(strategy, corr_df, base_df, "LTC/BTC")
|
||||
_, base_df = freqai.dd.get_base_and_corr_dataframes(sub_timerange, "LTC/BTC", freqai.dk)
|
||||
df = base_df[freqai_conf["timeframe"]]
|
||||
|
||||
pair = "ADA/BTC"
|
||||
metadata = {"pair": pair}
|
||||
freqai.dk.pair = pair
|
||||
freqai.start_backtesting(df, metadata, freqai.dk)
|
||||
freqai.start_backtesting(df, metadata, freqai.dk, strategy)
|
||||
|
||||
assert log_has_re(
|
||||
"Found backtesting prediction file ",
|
||||
@@ -338,7 +348,7 @@ def test_start_backtesting_from_existing_folder(mocker, freqai_conf, caplog):
|
||||
pair = "ETH/BTC"
|
||||
metadata = {"pair": pair}
|
||||
freqai.dk.pair = pair
|
||||
freqai.start_backtesting(df, metadata, freqai.dk)
|
||||
freqai.start_backtesting(df, metadata, freqai.dk, strategy)
|
||||
|
||||
path = (freqai.dd.full_path / freqai.dk.backtest_predictions_folder)
|
||||
prediction_files = [x for x in path.iterdir() if x.is_file()]
|
||||
@@ -372,57 +382,6 @@ def test_backtesting_fit_live_predictions(mocker, freqai_conf, caplog):
|
||||
shutil.rmtree(Path(freqai.dk.full_path))
|
||||
|
||||
|
||||
def test_follow_mode(mocker, freqai_conf):
|
||||
freqai_conf.update({"timerange": "20180110-20180130"})
|
||||
|
||||
strategy = get_patched_freqai_strategy(mocker, freqai_conf)
|
||||
exchange = get_patched_exchange(mocker, freqai_conf)
|
||||
strategy.dp = DataProvider(freqai_conf, exchange)
|
||||
strategy.freqai_info = freqai_conf.get("freqai", {})
|
||||
freqai = strategy.freqai
|
||||
freqai.live = True
|
||||
freqai.dk = FreqaiDataKitchen(freqai_conf)
|
||||
timerange = TimeRange.parse_timerange("20180110-20180130")
|
||||
freqai.dd.load_all_pair_histories(timerange, freqai.dk)
|
||||
|
||||
metadata = {"pair": "ADA/BTC"}
|
||||
freqai.dd.set_pair_dict_info(metadata)
|
||||
|
||||
data_load_timerange = TimeRange.parse_timerange("20180110-20180130")
|
||||
new_timerange = TimeRange.parse_timerange("20180120-20180130")
|
||||
|
||||
freqai.extract_data_and_train_model(
|
||||
new_timerange, "ADA/BTC", strategy, freqai.dk, data_load_timerange)
|
||||
|
||||
assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_model.joblib").is_file()
|
||||
assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_metadata.json").is_file()
|
||||
assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_trained_df.pkl").is_file()
|
||||
assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_svm_model.joblib").is_file()
|
||||
|
||||
# start the follower and ask it to predict on existing files
|
||||
|
||||
freqai_conf.get("freqai", {}).update({"follow_mode": "true"})
|
||||
|
||||
strategy = get_patched_freqai_strategy(mocker, freqai_conf)
|
||||
exchange = get_patched_exchange(mocker, freqai_conf)
|
||||
strategy.dp = DataProvider(freqai_conf, exchange)
|
||||
strategy.freqai_info = freqai_conf.get("freqai", {})
|
||||
freqai = strategy.freqai
|
||||
freqai.live = True
|
||||
freqai.dk = FreqaiDataKitchen(freqai_conf, freqai.live)
|
||||
timerange = TimeRange.parse_timerange("20180110-20180130")
|
||||
freqai.dd.load_all_pair_histories(timerange, freqai.dk)
|
||||
|
||||
df = strategy.dp.get_pair_dataframe('ADA/BTC', '5m')
|
||||
|
||||
freqai.dk.pair = "ADA/BTC"
|
||||
freqai.start_live(df, metadata, strategy, freqai.dk)
|
||||
|
||||
assert len(freqai.dk.return_dataframe.index) == 5702
|
||||
|
||||
shutil.rmtree(Path(freqai.dk.full_path))
|
||||
|
||||
|
||||
def test_principal_component_analysis(mocker, freqai_conf):
|
||||
freqai_conf.update({"timerange": "20180110-20180130"})
|
||||
freqai_conf.get("freqai", {}).get("feature_parameters", {}).update(
|
||||
@@ -553,6 +512,8 @@ def test_get_state_info(mocker, freqai_conf, dp_exists, caplog, tickers):
|
||||
|
||||
if is_mac():
|
||||
pytest.skip("Reinforcement learning module not available on intel based Mac OS")
|
||||
if is_py11():
|
||||
pytest.skip("Reinforcement learning currently not available on python 3.11.")
|
||||
|
||||
freqai_conf.update({"freqaimodel": "ReinforcementLearner"})
|
||||
freqai_conf.update({"timerange": "20180110-20180130"})
|
||||
@@ -564,7 +525,7 @@ def test_get_state_info(mocker, freqai_conf, dp_exists, caplog, tickers):
|
||||
strategy = get_patched_freqai_strategy(mocker, freqai_conf)
|
||||
exchange = get_patched_exchange(mocker, freqai_conf)
|
||||
ticker_mock = MagicMock(return_value=tickers()['ETH/BTC'])
|
||||
mocker.patch("freqtrade.exchange.Exchange.fetch_ticker", ticker_mock)
|
||||
mocker.patch(f"{EXMS}.fetch_ticker", ticker_mock)
|
||||
strategy.dp = DataProvider(freqai_conf, exchange)
|
||||
|
||||
if not dp_exists:
|
||||
|
65
tests/freqai/test_models/ReinforcementLearner_test_3ac.py
Normal file
65
tests/freqai/test_models/ReinforcementLearner_test_3ac.py
Normal file
@@ -0,0 +1,65 @@
|
||||
import logging
|
||||
|
||||
import numpy as np
|
||||
|
||||
from freqtrade.freqai.prediction_models.ReinforcementLearner import ReinforcementLearner
|
||||
from freqtrade.freqai.RL.Base3ActionRLEnv import Actions, Base3ActionRLEnv, Positions
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ReinforcementLearner_test_3ac(ReinforcementLearner):
|
||||
"""
|
||||
User created Reinforcement Learning Model prediction model.
|
||||
"""
|
||||
|
||||
class MyRLEnv(Base3ActionRLEnv):
|
||||
"""
|
||||
User can override any function in BaseRLEnv and gym.Env. Here the user
|
||||
sets a custom reward based on profit and trade duration.
|
||||
"""
|
||||
|
||||
def calculate_reward(self, action: int) -> float:
|
||||
|
||||
# first, penalize if the action is not valid
|
||||
if not self._is_valid(action):
|
||||
return -2
|
||||
|
||||
pnl = self.get_unrealized_profit()
|
||||
rew = np.sign(pnl) * (pnl + 1)
|
||||
factor = 100.
|
||||
|
||||
# reward agent for entering trades
|
||||
if (action in (Actions.Buy.value, Actions.Sell.value)
|
||||
and self._position == Positions.Neutral):
|
||||
return 25
|
||||
# discourage agent from not entering trades
|
||||
if action == Actions.Neutral.value and self._position == Positions.Neutral:
|
||||
return -1
|
||||
|
||||
max_trade_duration = self.rl_config.get('max_trade_duration_candles', 300)
|
||||
trade_duration = self._current_tick - self._last_trade_tick # type: ignore
|
||||
|
||||
if trade_duration <= max_trade_duration:
|
||||
factor *= 1.5
|
||||
elif trade_duration > max_trade_duration:
|
||||
factor *= 0.5
|
||||
|
||||
# discourage sitting in position
|
||||
if self._position in (Positions.Short, Positions.Long) and (
|
||||
action == Actions.Neutral.value
|
||||
or (action == Actions.Sell.value and self._position == Positions.Short)
|
||||
or (action == Actions.Buy.value and self._position == Positions.Long)
|
||||
):
|
||||
return -1 * trade_duration / max_trade_duration
|
||||
|
||||
# close position
|
||||
if (action == Actions.Buy.value and self._position == Positions.Short) or (
|
||||
action == Actions.Sell.value and self._position == Positions.Long
|
||||
):
|
||||
if pnl > self.profit_aim * self.rr:
|
||||
factor *= self.rl_config["model_reward_parameters"].get("win_reward_factor", 2)
|
||||
return float(rew * factor)
|
||||
|
||||
return 0.
|
@@ -1,5 +1,6 @@
|
||||
import pytest
|
||||
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.leverage import interest
|
||||
from freqtrade.util import FtPrecise
|
||||
|
||||
@@ -29,3 +30,13 @@ def test_interest(exchange, interest_rate, hours, expected):
|
||||
rate=FtPrecise(interest_rate),
|
||||
hours=hours
|
||||
))) == expected
|
||||
|
||||
|
||||
def test_interest_exception():
|
||||
with pytest.raises(OperationalException, match=r"Leverage not available on .* with freqtrade"):
|
||||
interest(
|
||||
exchange_name='bitmex',
|
||||
borrowed=FtPrecise(60.0),
|
||||
rate=FtPrecise(0.0005),
|
||||
hours=ten_mins
|
||||
)
|
||||
|
@@ -48,8 +48,8 @@ def hyperopt_results():
|
||||
return pd.DataFrame(
|
||||
{
|
||||
'pair': ['ETH/USDT', 'ETH/USDT', 'ETH/USDT', 'ETH/USDT'],
|
||||
'profit_ratio': [-0.1, 0.2, -0.1, 0.3],
|
||||
'profit_abs': [-0.2, 0.4, -0.2, 0.6],
|
||||
'profit_ratio': [-0.1, 0.2, -0.12, 0.3],
|
||||
'profit_abs': [-0.2, 0.4, -0.21, 0.6],
|
||||
'trade_duration': [10, 30, 10, 10],
|
||||
'amount': [0.1, 0.1, 0.1, 0.1],
|
||||
'exit_reason': [ExitType.STOP_LOSS, ExitType.ROI, ExitType.STOP_LOSS, ExitType.ROI],
|
||||
|
@@ -8,7 +8,7 @@ from freqtrade.data.history import get_timerange
|
||||
from freqtrade.enums import ExitType
|
||||
from freqtrade.optimize.backtesting import Backtesting
|
||||
from freqtrade.persistence.trade_model import LocalTrade
|
||||
from tests.conftest import patch_exchange
|
||||
from tests.conftest import EXMS, patch_exchange
|
||||
from tests.optimize import (BTContainer, BTrade, _build_backtest_dataframe,
|
||||
_get_frame_time_from_offset, tests_timeframe)
|
||||
|
||||
@@ -919,11 +919,12 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data: BTContainer)
|
||||
default_conf["trailing_stop_positive"] = data.trailing_stop_positive
|
||||
default_conf["trailing_stop_positive_offset"] = data.trailing_stop_positive_offset
|
||||
default_conf["use_exit_signal"] = data.use_exit_signal
|
||||
default_conf["max_open_trades"] = 10
|
||||
|
||||
mocker.patch("freqtrade.exchange.Exchange.get_fee", return_value=0.0)
|
||||
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
|
||||
mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf'))
|
||||
mocker.patch("freqtrade.exchange.Binance.get_max_leverage", return_value=100)
|
||||
mocker.patch(f"{EXMS}.get_fee", return_value=0.0)
|
||||
mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001)
|
||||
mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf'))
|
||||
mocker.patch(f"{EXMS}.get_max_leverage", return_value=100)
|
||||
patch_exchange(mocker)
|
||||
frame = _build_backtest_dataframe(data.data)
|
||||
backtesting = Backtesting(default_conf)
|
||||
@@ -951,7 +952,6 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data: BTContainer)
|
||||
processed=data_processed,
|
||||
start_date=min_date,
|
||||
end_date=max_date,
|
||||
max_open_trades=10,
|
||||
)
|
||||
|
||||
results = result['results']
|
||||
|
@@ -19,15 +19,15 @@ from freqtrade.data.btanalysis import BT_DATA_COLUMNS, evaluate_result_multi
|
||||
from freqtrade.data.converter import clean_ohlcv_dataframe
|
||||
from freqtrade.data.dataprovider import DataProvider
|
||||
from freqtrade.data.history import get_timerange
|
||||
from freqtrade.enums import ExitType, RunMode
|
||||
from freqtrade.enums import CandleType, ExitType, RunMode
|
||||
from freqtrade.exceptions import DependencyException, OperationalException
|
||||
from freqtrade.exchange.exchange import timeframe_to_next_date
|
||||
from freqtrade.optimize.backtest_caching import get_strategy_run_id
|
||||
from freqtrade.optimize.backtesting import Backtesting
|
||||
from freqtrade.persistence import LocalTrade
|
||||
from freqtrade.persistence import LocalTrade, Trade
|
||||
from freqtrade.resolvers import StrategyResolver
|
||||
from tests.conftest import (CURRENT_TEST_STRATEGY, get_args, log_has, log_has_re, patch_exchange,
|
||||
patched_configuration_load_config_file)
|
||||
from tests.conftest import (CURRENT_TEST_STRATEGY, EXMS, get_args, log_has, log_has_re,
|
||||
patch_exchange, patched_configuration_load_config_file)
|
||||
|
||||
|
||||
ORDER_TYPES = [
|
||||
@@ -96,7 +96,6 @@ def _make_backtest_conf(mocker, datadir, conf=None, pair='UNITTEST/BTC'):
|
||||
'processed': processed,
|
||||
'start_date': min_date,
|
||||
'end_date': max_date,
|
||||
'max_open_trades': 10,
|
||||
}
|
||||
|
||||
|
||||
@@ -246,7 +245,7 @@ def test_setup_optimize_configuration_stake_amount(mocker, default_conf, caplog)
|
||||
|
||||
def test_start(mocker, fee, default_conf, caplog) -> None:
|
||||
start_mock = MagicMock()
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||
mocker.patch(f'{EXMS}.get_fee', fee)
|
||||
patch_exchange(mocker)
|
||||
mocker.patch('freqtrade.optimize.backtesting.Backtesting.start', start_mock)
|
||||
patched_configuration_load_config_file(mocker, default_conf)
|
||||
@@ -270,7 +269,7 @@ def test_backtesting_init(mocker, default_conf, order_types) -> None:
|
||||
"""
|
||||
default_conf["order_types"] = order_types
|
||||
patch_exchange(mocker)
|
||||
get_fee = mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.5))
|
||||
get_fee = mocker.patch(f'{EXMS}.get_fee', MagicMock(return_value=0.5))
|
||||
backtesting = Backtesting(default_conf)
|
||||
backtesting._set_strategy(backtesting.strategylist[0])
|
||||
assert backtesting.config == default_conf
|
||||
@@ -291,7 +290,7 @@ def test_backtesting_init_no_timeframe(mocker, default_conf, caplog) -> None:
|
||||
default_conf['strategy_list'] = [CURRENT_TEST_STRATEGY,
|
||||
'HyperoptableStrategy']
|
||||
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.5))
|
||||
mocker.patch(f'{EXMS}.get_fee', MagicMock(return_value=0.5))
|
||||
with pytest.raises(OperationalException,
|
||||
match=r"Timeframe needs to be set in either configuration"):
|
||||
Backtesting(default_conf)
|
||||
@@ -301,7 +300,7 @@ def test_data_with_fee(default_conf, mocker) -> None:
|
||||
patch_exchange(mocker)
|
||||
default_conf['fee'] = 0.1234
|
||||
|
||||
fee_mock = mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.5))
|
||||
fee_mock = mocker.patch(f'{EXMS}.get_fee', MagicMock(return_value=0.5))
|
||||
backtesting = Backtesting(default_conf)
|
||||
backtesting._set_strategy(backtesting.strategylist[0])
|
||||
assert backtesting.fee == 0.1234
|
||||
@@ -360,7 +359,6 @@ def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None:
|
||||
PropertyMock(return_value=['UNITTEST/BTC']))
|
||||
|
||||
default_conf['timeframe'] = '1m'
|
||||
default_conf['datadir'] = testdatadir
|
||||
default_conf['export'] = 'signals'
|
||||
default_conf['exportfilename'] = 'export.txt'
|
||||
default_conf['timerange'] = '-1510694220'
|
||||
@@ -396,7 +394,6 @@ def test_backtesting_start_no_data(default_conf, mocker, caplog, testdatadir) ->
|
||||
PropertyMock(return_value=['UNITTEST/BTC']))
|
||||
|
||||
default_conf['timeframe'] = "1m"
|
||||
default_conf['datadir'] = testdatadir
|
||||
default_conf['export'] = 'none'
|
||||
default_conf['timerange'] = '20180101-20180102'
|
||||
|
||||
@@ -407,7 +404,7 @@ def test_backtesting_start_no_data(default_conf, mocker, caplog, testdatadir) ->
|
||||
|
||||
|
||||
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(f'{EXMS}.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)
|
||||
@@ -417,7 +414,6 @@ def test_backtesting_no_pair_left(default_conf, mocker, caplog, testdatadir) ->
|
||||
PropertyMock(return_value=[]))
|
||||
|
||||
default_conf['timeframe'] = "1m"
|
||||
default_conf['datadir'] = testdatadir
|
||||
default_conf['export'] = 'none'
|
||||
default_conf['timerange'] = '20180101-20180102'
|
||||
|
||||
@@ -440,9 +436,9 @@ def test_backtesting_no_pair_left(default_conf, mocker, caplog, testdatadir) ->
|
||||
|
||||
|
||||
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(f'{EXMS}.exchange_has', MagicMock(return_value=True))
|
||||
mocker.patch(f'{EXMS}.get_tickers', tickers)
|
||||
mocker.patch(f'{EXMS}.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')
|
||||
@@ -451,7 +447,6 @@ def test_backtesting_pairlist_list(default_conf, mocker, caplog, testdatadir, ti
|
||||
mocker.patch('freqtrade.plugins.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']
|
||||
@@ -479,9 +474,9 @@ def test_backtesting_pairlist_list(default_conf, mocker, caplog, testdatadir, ti
|
||||
|
||||
def test_backtest__enter_trade(default_conf, fee, mocker) -> None:
|
||||
default_conf['use_exit_signal'] = False
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
|
||||
mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf'))
|
||||
mocker.patch(f'{EXMS}.get_fee', fee)
|
||||
mocker.patch(f'{EXMS}.get_min_pair_stake_amount', return_value=0.00001)
|
||||
mocker.patch(f'{EXMS}.get_max_pair_stake_amount', return_value=float('inf'))
|
||||
patch_exchange(mocker)
|
||||
default_conf['stake_amount'] = 'unlimited'
|
||||
default_conf['max_open_trades'] = 2
|
||||
@@ -530,7 +525,7 @@ def test_backtest__enter_trade(default_conf, fee, mocker) -> None:
|
||||
assert trade.stake_amount == 495
|
||||
assert trade.is_short is True
|
||||
|
||||
mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=300.0)
|
||||
mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=300.0)
|
||||
trade = backtesting._enter_trade(pair, row=row, direction='long')
|
||||
assert trade
|
||||
assert trade.stake_amount == 300.0
|
||||
@@ -538,10 +533,10 @@ def test_backtest__enter_trade(default_conf, fee, mocker) -> None:
|
||||
|
||||
def test_backtest__enter_trade_futures(default_conf_usdt, fee, mocker) -> None:
|
||||
default_conf_usdt['use_exit_signal'] = False
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
|
||||
mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf'))
|
||||
mocker.patch("freqtrade.exchange.Exchange.get_max_leverage", return_value=100)
|
||||
mocker.patch(f'{EXMS}.get_fee', fee)
|
||||
mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001)
|
||||
mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf'))
|
||||
mocker.patch(f"{EXMS}.get_max_leverage", return_value=100)
|
||||
mocker.patch("freqtrade.optimize.backtesting.price_to_precision", lambda p, *args: p)
|
||||
patch_exchange(mocker)
|
||||
default_conf_usdt['stake_amount'] = 300
|
||||
@@ -569,7 +564,7 @@ def test_backtest__enter_trade_futures(default_conf_usdt, fee, mocker) -> None:
|
||||
]
|
||||
|
||||
backtesting.strategy.leverage = MagicMock(return_value=5.0)
|
||||
mocker.patch("freqtrade.exchange.Exchange.get_maintenance_ratio_and_amt",
|
||||
mocker.patch(f"{EXMS}.get_maintenance_ratio_and_amt",
|
||||
return_value=(0.01, 0.01))
|
||||
|
||||
# leverage = 5
|
||||
@@ -606,7 +601,7 @@ def test_backtest__enter_trade_futures(default_conf_usdt, fee, mocker) -> None:
|
||||
assert pytest.approx(trade.liquidation_price) == 0.11787191
|
||||
|
||||
# Stake-amount too high!
|
||||
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=600.0)
|
||||
mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=600.0)
|
||||
|
||||
trade = backtesting._enter_trade(pair, row=row, direction='long')
|
||||
assert trade is None
|
||||
@@ -619,11 +614,11 @@ def test_backtest__enter_trade_futures(default_conf_usdt, fee, mocker) -> None:
|
||||
assert trade is None
|
||||
|
||||
|
||||
def test_backtest__get_sell_trade_entry(default_conf, fee, mocker) -> None:
|
||||
def test_backtest__check_trade_exit(default_conf, fee, mocker) -> None:
|
||||
default_conf['use_exit_signal'] = False
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
|
||||
mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf'))
|
||||
mocker.patch(f'{EXMS}.get_fee', fee)
|
||||
mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001)
|
||||
mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf'))
|
||||
patch_exchange(mocker)
|
||||
default_conf['timeframe_detail'] = '1m'
|
||||
default_conf['max_open_trades'] = 2
|
||||
@@ -665,7 +660,7 @@ def test_backtest__get_sell_trade_entry(default_conf, fee, mocker) -> None:
|
||||
]
|
||||
|
||||
# No data available.
|
||||
res = backtesting._get_exit_trade_entry(trade, row_sell, True)
|
||||
res = backtesting._check_trade_exit(trade, row_sell)
|
||||
assert res is not None
|
||||
assert res.exit_reason == ExitType.ROI.value
|
||||
assert res.close_date_utc == datetime(2020, 1, 1, 5, 0, tzinfo=timezone.utc)
|
||||
@@ -678,15 +673,17 @@ def test_backtest__get_sell_trade_entry(default_conf, fee, mocker) -> None:
|
||||
[], columns=['date', 'open', 'high', 'low', 'close', 'enter_long', 'exit_long',
|
||||
'enter_short', 'exit_short', 'long_tag', 'short_tag', 'exit_tag'])
|
||||
|
||||
res = backtesting._get_exit_trade_entry(trade, row, True)
|
||||
res = backtesting._check_trade_exit(trade, row)
|
||||
assert res is None
|
||||
|
||||
|
||||
def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None:
|
||||
default_conf['use_exit_signal'] = False
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
|
||||
mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf'))
|
||||
default_conf['max_open_trades'] = 10
|
||||
|
||||
mocker.patch(f'{EXMS}.get_fee', fee)
|
||||
mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001)
|
||||
mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf'))
|
||||
patch_exchange(mocker)
|
||||
backtesting = Backtesting(default_conf)
|
||||
backtesting._set_strategy(backtesting.strategylist[0])
|
||||
@@ -701,7 +698,6 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None:
|
||||
processed=deepcopy(processed),
|
||||
start_date=min_date,
|
||||
end_date=max_date,
|
||||
max_open_trades=10,
|
||||
)
|
||||
results = result['results']
|
||||
assert not results.empty
|
||||
@@ -710,6 +706,7 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None:
|
||||
expected = pd.DataFrame(
|
||||
{'pair': [pair, pair],
|
||||
'stake_amount': [0.001, 0.001],
|
||||
'max_stake_amount': [0.001, 0.001],
|
||||
'amount': [0.00957442, 0.0097064],
|
||||
'open_date': pd.to_datetime([Arrow(2018, 1, 29, 18, 40, 0).datetime,
|
||||
Arrow(2018, 1, 30, 3, 30, 0).datetime], utc=True
|
||||
@@ -769,9 +766,9 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None:
|
||||
@pytest.mark.parametrize('use_detail', [True, False])
|
||||
def test_backtest_one_detail(default_conf_usdt, fee, mocker, testdatadir, use_detail) -> None:
|
||||
default_conf_usdt['use_exit_signal'] = False
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
|
||||
mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf'))
|
||||
mocker.patch(f'{EXMS}.get_fee', fee)
|
||||
mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001)
|
||||
mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf'))
|
||||
if use_detail:
|
||||
default_conf_usdt['timeframe_detail'] = '1m'
|
||||
patch_exchange(mocker)
|
||||
@@ -784,6 +781,8 @@ def test_backtest_one_detail(default_conf_usdt, fee, mocker, testdatadir, use_de
|
||||
def custom_entry_price(proposed_rate, **kwargs):
|
||||
return proposed_rate * 0.997
|
||||
|
||||
default_conf_usdt['max_open_trades'] = 10
|
||||
|
||||
backtesting = Backtesting(default_conf_usdt)
|
||||
backtesting._set_strategy(backtesting.strategylist[0])
|
||||
backtesting.strategy.populate_entry_trend = advise_entry
|
||||
@@ -791,10 +790,10 @@ def test_backtest_one_detail(default_conf_usdt, fee, mocker, testdatadir, use_de
|
||||
pair = 'XRP/ETH'
|
||||
# Pick a timerange adapted to the pair we use to test
|
||||
timerange = TimeRange.parse_timerange('20191010-20191013')
|
||||
data = history.load_data(datadir=testdatadir, timeframe='5m', pairs=['XRP/ETH'],
|
||||
data = history.load_data(datadir=testdatadir, timeframe='5m', pairs=[pair],
|
||||
timerange=timerange)
|
||||
if use_detail:
|
||||
data_1m = history.load_data(datadir=testdatadir, timeframe='1m', pairs=['XRP/ETH'],
|
||||
data_1m = history.load_data(datadir=testdatadir, timeframe='1m', pairs=[pair],
|
||||
timerange=timerange)
|
||||
backtesting.detail_data = data_1m
|
||||
processed = backtesting.strategy.advise_all_indicators(data)
|
||||
@@ -804,7 +803,6 @@ def test_backtest_one_detail(default_conf_usdt, fee, mocker, testdatadir, use_de
|
||||
processed=deepcopy(processed),
|
||||
start_date=min_date,
|
||||
end_date=max_date,
|
||||
max_open_trades=10,
|
||||
)
|
||||
results = result['results']
|
||||
assert not results.empty
|
||||
@@ -848,16 +846,175 @@ def test_backtest_one_detail(default_conf_usdt, fee, mocker, testdatadir, use_de
|
||||
assert late_entry > 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize('use_detail', [True, False])
|
||||
def test_backtest_one_detail_futures(
|
||||
default_conf_usdt, fee, mocker, testdatadir, use_detail) -> None:
|
||||
default_conf_usdt['use_exit_signal'] = False
|
||||
default_conf_usdt['trading_mode'] = 'futures'
|
||||
default_conf_usdt['margin_mode'] = 'isolated'
|
||||
default_conf_usdt['candle_type_def'] = CandleType.FUTURES
|
||||
|
||||
mocker.patch(f'{EXMS}.get_fee', fee)
|
||||
mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001)
|
||||
mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf'))
|
||||
mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist',
|
||||
PropertyMock(return_value=['XRP/USDT:USDT']))
|
||||
mocker.patch(f"{EXMS}.get_maintenance_ratio_and_amt",
|
||||
return_value=(0.01, 0.01))
|
||||
default_conf_usdt['timeframe'] = '1h'
|
||||
if use_detail:
|
||||
default_conf_usdt['timeframe_detail'] = '5m'
|
||||
patch_exchange(mocker)
|
||||
|
||||
def advise_entry(df, *args, **kwargs):
|
||||
# Mock function to force several entries
|
||||
df.loc[(df['rsi'] < 40), 'enter_long'] = 1
|
||||
return df
|
||||
|
||||
def custom_entry_price(proposed_rate, **kwargs):
|
||||
return proposed_rate * 0.997
|
||||
|
||||
default_conf_usdt['max_open_trades'] = 10
|
||||
|
||||
backtesting = Backtesting(default_conf_usdt)
|
||||
backtesting._set_strategy(backtesting.strategylist[0])
|
||||
backtesting.strategy.populate_entry_trend = advise_entry
|
||||
backtesting.strategy.custom_entry_price = custom_entry_price
|
||||
pair = 'XRP/USDT:USDT'
|
||||
# Pick a timerange adapted to the pair we use to test
|
||||
timerange = TimeRange.parse_timerange('20211117-20211119')
|
||||
data = history.load_data(datadir=Path(testdatadir), timeframe='1h', pairs=[pair],
|
||||
timerange=timerange, candle_type=CandleType.FUTURES)
|
||||
backtesting.load_bt_data_detail()
|
||||
processed = backtesting.strategy.advise_all_indicators(data)
|
||||
min_date, max_date = get_timerange(processed)
|
||||
|
||||
result = backtesting.backtest(
|
||||
processed=deepcopy(processed),
|
||||
start_date=min_date,
|
||||
end_date=max_date,
|
||||
)
|
||||
results = result['results']
|
||||
assert not results.empty
|
||||
# Timeout settings from default_conf = entry: 10, exit: 30
|
||||
assert len(results) == (5 if use_detail else 2)
|
||||
|
||||
assert 'orders' in results.columns
|
||||
data_pair = processed[pair]
|
||||
|
||||
data_1m_pair = backtesting.detail_data[pair] if use_detail else pd.DataFrame()
|
||||
late_entry = 0
|
||||
for _, t in results.iterrows():
|
||||
assert len(t['orders']) == 2
|
||||
|
||||
entryo = t['orders'][0]
|
||||
entry_ts = datetime.fromtimestamp(entryo['order_filled_timestamp'] // 1000, tz=timezone.utc)
|
||||
if entry_ts > t['open_date']:
|
||||
late_entry += 1
|
||||
|
||||
# Get "entry fill" candle
|
||||
ln = (data_1m_pair.loc[data_1m_pair["date"] == entry_ts]
|
||||
if use_detail else data_pair.loc[data_pair["date"] == entry_ts])
|
||||
# Check open trade rate aligns to open rate
|
||||
assert not ln.empty
|
||||
|
||||
assert round(ln.iloc[0]["low"], 6) <= round(
|
||||
t["open_rate"], 6) <= round(ln.iloc[0]["high"], 6)
|
||||
# check close trade rate aligns to close rate or is between high and low
|
||||
ln1 = data_pair.loc[data_pair["date"] == t["close_date"]]
|
||||
if use_detail:
|
||||
ln1_1m = data_1m_pair.loc[data_1m_pair["date"] == t["close_date"]]
|
||||
assert not ln1.empty or not ln1_1m.empty
|
||||
else:
|
||||
assert not ln1.empty
|
||||
ln2 = ln1_1m if ln1.empty else ln1
|
||||
|
||||
assert (round(ln2.iloc[0]["low"], 6) <= round(
|
||||
t["close_rate"], 6) <= round(ln2.iloc[0]["high"], 6))
|
||||
assert -0.0181 < Trade.trades[1].funding_fees < -0.01
|
||||
# assert late_entry > 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize('use_detail', [True, False])
|
||||
def test_backtest_one_detail_futures_funding_fees(
|
||||
default_conf_usdt, fee, mocker, testdatadir, use_detail) -> None:
|
||||
default_conf_usdt['use_exit_signal'] = False
|
||||
default_conf_usdt['trading_mode'] = 'futures'
|
||||
default_conf_usdt['margin_mode'] = 'isolated'
|
||||
default_conf_usdt['candle_type_def'] = CandleType.FUTURES
|
||||
default_conf_usdt['minimal_roi'] = {'0': 1}
|
||||
default_conf_usdt['dry_run_wallet'] = 100000
|
||||
|
||||
mocker.patch(f'{EXMS}.get_fee', fee)
|
||||
mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001)
|
||||
mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf'))
|
||||
mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist',
|
||||
PropertyMock(return_value=['XRP/USDT:USDT']))
|
||||
mocker.patch(f"{EXMS}.get_maintenance_ratio_and_amt",
|
||||
return_value=(0.01, 0.01))
|
||||
default_conf_usdt['timeframe'] = '1h'
|
||||
if use_detail:
|
||||
default_conf_usdt['timeframe_detail'] = '5m'
|
||||
patch_exchange(mocker)
|
||||
|
||||
def advise_entry(df, *args, **kwargs):
|
||||
# Mock function to force several entries
|
||||
df.loc[:, 'enter_long'] = 1
|
||||
return df
|
||||
|
||||
def adjust_trade_position(trade, current_time, **kwargs):
|
||||
if current_time > datetime(2021, 11, 18, 2, 0, 0, tzinfo=timezone.utc):
|
||||
return None
|
||||
return default_conf_usdt['stake_amount']
|
||||
|
||||
default_conf_usdt['max_open_trades'] = 1
|
||||
|
||||
backtesting = Backtesting(default_conf_usdt)
|
||||
backtesting._set_strategy(backtesting.strategylist[0])
|
||||
backtesting.strategy.populate_entry_trend = advise_entry
|
||||
backtesting.strategy.adjust_trade_position = adjust_trade_position
|
||||
backtesting.strategy.leverage = lambda **kwargs: 1
|
||||
backtesting.strategy.position_adjustment_enable = True
|
||||
pair = 'XRP/USDT:USDT'
|
||||
# Pick a timerange adapted to the pair we use to test
|
||||
timerange = TimeRange.parse_timerange('20211117-20211119')
|
||||
data = history.load_data(datadir=Path(testdatadir), timeframe='1h', pairs=[pair],
|
||||
timerange=timerange, candle_type=CandleType.FUTURES)
|
||||
backtesting.load_bt_data_detail()
|
||||
processed = backtesting.strategy.advise_all_indicators(data)
|
||||
min_date, max_date = get_timerange(processed)
|
||||
|
||||
result = backtesting.backtest(
|
||||
processed=deepcopy(processed),
|
||||
start_date=min_date,
|
||||
end_date=max_date,
|
||||
)
|
||||
results = result['results']
|
||||
assert not results.empty
|
||||
# Only one result - as we're not selling.
|
||||
assert len(results) == 1
|
||||
|
||||
assert 'orders' in results.columns
|
||||
|
||||
for t in Trade.trades:
|
||||
# At least 4 adjustment orders
|
||||
assert t.nr_of_successful_entries >= 6
|
||||
# Funding fees will vary depending on the number of adjustment orders
|
||||
# That number is a lot higher with detail data.
|
||||
assert -20 < t.funding_fees < -0.1
|
||||
|
||||
|
||||
def test_backtest_timedout_entry_orders(default_conf, fee, mocker, testdatadir) -> None:
|
||||
# This strategy intentionally places unfillable orders.
|
||||
default_conf['strategy'] = 'StrategyTestV3CustomEntryPrice'
|
||||
default_conf['startup_candle_count'] = 0
|
||||
# Cancel unfilled order after 4 minutes on 5m timeframe.
|
||||
default_conf["unfilledtimeout"] = {"entry": 4}
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
|
||||
mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf'))
|
||||
mocker.patch(f'{EXMS}.get_fee', fee)
|
||||
mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001)
|
||||
mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf'))
|
||||
patch_exchange(mocker)
|
||||
default_conf['max_open_trades'] = 1
|
||||
backtesting = Backtesting(default_conf)
|
||||
backtesting._set_strategy(backtesting.strategylist[0])
|
||||
# Testing dataframe contains 11 candles. Expecting 10 timed out orders.
|
||||
@@ -870,7 +1027,6 @@ def test_backtest_timedout_entry_orders(default_conf, fee, mocker, testdatadir)
|
||||
processed=deepcopy(data),
|
||||
start_date=min_date,
|
||||
end_date=max_date,
|
||||
max_open_trades=1,
|
||||
)
|
||||
|
||||
assert result['timedout_entry_orders'] == 10
|
||||
@@ -878,9 +1034,10 @@ def test_backtest_timedout_entry_orders(default_conf, fee, mocker, testdatadir)
|
||||
|
||||
def test_backtest_1min_timeframe(default_conf, fee, mocker, testdatadir) -> None:
|
||||
default_conf['use_exit_signal'] = False
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
|
||||
mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf'))
|
||||
default_conf['max_open_trades'] = 1
|
||||
mocker.patch(f'{EXMS}.get_fee', fee)
|
||||
mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001)
|
||||
mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf'))
|
||||
patch_exchange(mocker)
|
||||
backtesting = Backtesting(default_conf)
|
||||
backtesting._set_strategy(backtesting.strategylist[0])
|
||||
@@ -895,7 +1052,6 @@ def test_backtest_1min_timeframe(default_conf, fee, mocker, testdatadir) -> None
|
||||
processed=processed,
|
||||
start_date=min_date,
|
||||
end_date=max_date,
|
||||
max_open_trades=1,
|
||||
)
|
||||
assert not results['results'].empty
|
||||
assert len(results['results']) == 1
|
||||
@@ -903,9 +1059,11 @@ def test_backtest_1min_timeframe(default_conf, fee, mocker, testdatadir) -> None
|
||||
|
||||
def test_backtest_trim_no_data_left(default_conf, fee, mocker, testdatadir) -> None:
|
||||
default_conf['use_exit_signal'] = False
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
|
||||
mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf'))
|
||||
default_conf['max_open_trades'] = 10
|
||||
|
||||
mocker.patch(f'{EXMS}.get_fee', fee)
|
||||
mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001)
|
||||
mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf'))
|
||||
patch_exchange(mocker)
|
||||
backtesting = Backtesting(default_conf)
|
||||
backtesting._set_strategy(backtesting.strategylist[0])
|
||||
@@ -926,7 +1084,6 @@ def test_backtest_trim_no_data_left(default_conf, fee, mocker, testdatadir) -> N
|
||||
processed=deepcopy(processed),
|
||||
start_date=min_date,
|
||||
end_date=max_date,
|
||||
max_open_trades=10,
|
||||
)
|
||||
|
||||
|
||||
@@ -947,9 +1104,10 @@ def test_processed(default_conf, mocker, testdatadir) -> None:
|
||||
|
||||
def test_backtest_dataprovider_analyzed_df(default_conf, fee, mocker, testdatadir) -> None:
|
||||
default_conf['use_exit_signal'] = False
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
|
||||
mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=100000)
|
||||
default_conf['max_open_trades'] = 10
|
||||
mocker.patch(f'{EXMS}.get_fee', fee)
|
||||
mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001)
|
||||
mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=100000)
|
||||
patch_exchange(mocker)
|
||||
backtesting = Backtesting(default_conf)
|
||||
backtesting._set_strategy(backtesting.strategylist[0])
|
||||
@@ -980,7 +1138,6 @@ def test_backtest_dataprovider_analyzed_df(default_conf, fee, mocker, testdatadi
|
||||
processed=deepcopy(processed),
|
||||
start_date=min_date,
|
||||
end_date=max_date,
|
||||
max_open_trades=10,
|
||||
)
|
||||
assert count == 5
|
||||
|
||||
@@ -997,9 +1154,10 @@ def test_backtest_pricecontours_protections(default_conf, fee, mocker, testdatad
|
||||
|
||||
default_conf['enable_protections'] = True
|
||||
default_conf['timeframe'] = '1m'
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
|
||||
mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf'))
|
||||
default_conf['max_open_trades'] = 1
|
||||
mocker.patch(f'{EXMS}.get_fee', fee)
|
||||
mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001)
|
||||
mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf'))
|
||||
tests = [
|
||||
['sine', 9],
|
||||
['raise', 10],
|
||||
@@ -1023,7 +1181,6 @@ def test_backtest_pricecontours_protections(default_conf, fee, mocker, testdatad
|
||||
processed=processed,
|
||||
start_date=min_date,
|
||||
end_date=max_date,
|
||||
max_open_trades=1,
|
||||
)
|
||||
assert len(results['results']) == numres
|
||||
|
||||
@@ -1046,9 +1203,9 @@ def test_backtest_pricecontours(default_conf, fee, mocker, testdatadir,
|
||||
default_conf['protections'] = protections
|
||||
default_conf['enable_protections'] = True
|
||||
|
||||
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
|
||||
mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf'))
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||
mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001)
|
||||
mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf'))
|
||||
mocker.patch(f'{EXMS}.get_fee', fee)
|
||||
# While entry-signals are unrealistic, running backtesting
|
||||
# over and over again should not cause different results
|
||||
|
||||
@@ -1061,11 +1218,12 @@ def test_backtest_pricecontours(default_conf, fee, mocker, testdatadir,
|
||||
processed = backtesting.strategy.advise_all_indicators(data)
|
||||
min_date, max_date = get_timerange(processed)
|
||||
assert isinstance(processed, dict)
|
||||
backtesting.strategy.max_open_trades = 1
|
||||
backtesting.config.update({'max_open_trades': 1})
|
||||
results = backtesting.backtest(
|
||||
processed=processed,
|
||||
start_date=min_date,
|
||||
end_date=max_date,
|
||||
max_open_trades=1,
|
||||
)
|
||||
assert len(results['results']) == expected
|
||||
|
||||
@@ -1076,7 +1234,7 @@ def test_backtest_clash_buy_sell(mocker, default_conf, testdatadir):
|
||||
buy_value = 1
|
||||
sell_value = 1
|
||||
return _trend(dataframe, buy_value, sell_value)
|
||||
|
||||
default_conf['max_open_trades'] = 10
|
||||
backtest_conf = _make_backtest_conf(mocker, conf=default_conf, datadir=testdatadir)
|
||||
backtesting = Backtesting(default_conf)
|
||||
backtesting._set_strategy(backtesting.strategylist[0])
|
||||
@@ -1093,6 +1251,7 @@ def test_backtest_only_sell(mocker, default_conf, testdatadir):
|
||||
sell_value = 1
|
||||
return _trend(dataframe, buy_value, sell_value)
|
||||
|
||||
default_conf['max_open_trades'] = 10
|
||||
backtest_conf = _make_backtest_conf(mocker, conf=default_conf, datadir=testdatadir)
|
||||
backtesting = Backtesting(default_conf)
|
||||
backtesting._set_strategy(backtesting.strategylist[0])
|
||||
@@ -1103,9 +1262,10 @@ def test_backtest_only_sell(mocker, default_conf, testdatadir):
|
||||
|
||||
|
||||
def test_backtest_alternate_buy_sell(default_conf, fee, mocker, testdatadir):
|
||||
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
|
||||
mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf'))
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||
mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001)
|
||||
mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf'))
|
||||
mocker.patch(f'{EXMS}.get_fee', fee)
|
||||
default_conf['max_open_trades'] = 10
|
||||
backtest_conf = _make_backtest_conf(mocker, conf=default_conf,
|
||||
pair='UNITTEST/BTC', datadir=testdatadir)
|
||||
default_conf['timeframe'] = '1m'
|
||||
@@ -1150,9 +1310,9 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir)
|
||||
dataframe['exit_short'] = 0
|
||||
return dataframe
|
||||
|
||||
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
|
||||
mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf'))
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||
mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001)
|
||||
mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf'))
|
||||
mocker.patch(f'{EXMS}.get_fee', fee)
|
||||
patch_exchange(mocker)
|
||||
|
||||
pairs = ['ADA/BTC', 'DASH/BTC', 'ETH/BTC', 'LTC/BTC', 'NXT/BTC']
|
||||
@@ -1164,6 +1324,7 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir)
|
||||
if tres > 0:
|
||||
data[pair] = data[pair][tres:].reset_index()
|
||||
default_conf['timeframe'] = '5m'
|
||||
default_conf['max_open_trades'] = 3
|
||||
|
||||
backtesting = Backtesting(default_conf)
|
||||
backtesting._set_strategy(backtesting.strategylist[0])
|
||||
@@ -1172,11 +1333,11 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir)
|
||||
|
||||
processed = backtesting.strategy.advise_all_indicators(data)
|
||||
min_date, max_date = get_timerange(processed)
|
||||
|
||||
backtest_conf = {
|
||||
'processed': deepcopy(processed),
|
||||
'start_date': min_date,
|
||||
'end_date': max_date,
|
||||
'max_open_trades': 3,
|
||||
}
|
||||
|
||||
results = backtesting.backtest(**backtest_conf)
|
||||
@@ -1194,11 +1355,12 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir)
|
||||
backtesting.dataprovider.get_analyzed_dataframe('NXT/BTC', '5m')[0]
|
||||
) == len(data['NXT/BTC']) - 1 - backtesting.strategy.startup_candle_count
|
||||
|
||||
backtesting.strategy.max_open_trades = 1
|
||||
backtesting.config.update({'max_open_trades': 1})
|
||||
backtest_conf = {
|
||||
'processed': deepcopy(processed),
|
||||
'start_date': min_date,
|
||||
'end_date': max_date,
|
||||
'max_open_trades': 1,
|
||||
}
|
||||
results = backtesting.backtest(**backtest_conf)
|
||||
assert len(evaluate_result_multi(results['results'], '5m', 1)) == 0
|
||||
@@ -1459,7 +1621,7 @@ def test_backtest_start_futures_noliq(default_conf_usdt, mocker,
|
||||
patch_exchange(mocker)
|
||||
|
||||
mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist',
|
||||
PropertyMock(return_value=['HULUMULU/USDT', 'XRP/USDT']))
|
||||
PropertyMock(return_value=['HULUMULU/USDT', 'XRP/USDT:USDT']))
|
||||
# mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock)
|
||||
|
||||
patched_configuration_load_config_file(mocker, default_conf_usdt)
|
||||
@@ -1490,7 +1652,7 @@ def test_backtest_start_nomock_futures(default_conf_usdt, mocker,
|
||||
"strategy": CURRENT_TEST_STRATEGY,
|
||||
})
|
||||
patch_exchange(mocker)
|
||||
result1 = pd.DataFrame({'pair': ['XRP/USDT', 'XRP/USDT'],
|
||||
result1 = pd.DataFrame({'pair': ['XRP/USDT:USDT', 'XRP/USDT:USDT'],
|
||||
'profit_ratio': [0.0, 0.0],
|
||||
'profit_abs': [0.0, 0.0],
|
||||
'open_date': pd.to_datetime(['2021-11-18 18:00:00',
|
||||
@@ -1506,7 +1668,7 @@ def test_backtest_start_nomock_futures(default_conf_usdt, mocker,
|
||||
'close_rate': [0.104969, 0.103541],
|
||||
'exit_reason': [ExitType.ROI, ExitType.ROI]
|
||||
})
|
||||
result2 = pd.DataFrame({'pair': ['XRP/USDT', 'XRP/USDT', 'XRP/USDT'],
|
||||
result2 = pd.DataFrame({'pair': ['XRP/USDT:USDT', 'XRP/USDT:USDT', 'XRP/USDT:USDT'],
|
||||
'profit_ratio': [0.03, 0.01, 0.1],
|
||||
'profit_abs': [0.01, 0.02, 0.2],
|
||||
'open_date': pd.to_datetime(['2021-11-19 18:00:00',
|
||||
@@ -1551,7 +1713,7 @@ def test_backtest_start_nomock_futures(default_conf_usdt, mocker,
|
||||
}
|
||||
])
|
||||
mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist',
|
||||
PropertyMock(return_value=['XRP/USDT']))
|
||||
PropertyMock(return_value=['XRP/USDT:USDT']))
|
||||
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock)
|
||||
|
||||
patched_configuration_load_config_file(mocker, default_conf_usdt)
|
||||
@@ -1574,8 +1736,8 @@ def test_backtest_start_nomock_futures(default_conf_usdt, mocker,
|
||||
'up to 2021-11-21 04:00:00 (4 days).',
|
||||
'Backtesting with data from 2021-11-17 21:00:00 '
|
||||
'up to 2021-11-21 04:00:00 (3 days).',
|
||||
'XRP/USDT, funding_rate, 8h, data starts at 2021-11-18 00:00:00',
|
||||
'XRP/USDT, mark, 8h, data starts at 2021-11-18 00:00:00',
|
||||
'XRP/USDT:USDT, funding_rate, 8h, data starts at 2021-11-18 00:00:00',
|
||||
'XRP/USDT:USDT, mark, 8h, data starts at 2021-11-18 00:00:00',
|
||||
f'Running backtesting for Strategy {CURRENT_TEST_STRATEGY}',
|
||||
]
|
||||
|
||||
|
@@ -12,16 +12,17 @@ from freqtrade.data import history
|
||||
from freqtrade.data.history import get_timerange
|
||||
from freqtrade.enums import ExitType
|
||||
from freqtrade.optimize.backtesting import Backtesting
|
||||
from tests.conftest import patch_exchange
|
||||
from tests.conftest import EXMS, patch_exchange
|
||||
|
||||
|
||||
def test_backtest_position_adjustment(default_conf, fee, mocker, testdatadir) -> None:
|
||||
default_conf['use_exit_signal'] = False
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||
default_conf['max_open_trades'] = 10
|
||||
mocker.patch(f'{EXMS}.get_fee', fee)
|
||||
mocker.patch('freqtrade.optimize.backtesting.amount_to_contract_precision',
|
||||
lambda x, *args, **kwargs: round(x, 8))
|
||||
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
|
||||
mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf'))
|
||||
mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001)
|
||||
mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf'))
|
||||
patch_exchange(mocker)
|
||||
default_conf.update({
|
||||
"stake_amount": 100.0,
|
||||
@@ -41,7 +42,6 @@ def test_backtest_position_adjustment(default_conf, fee, mocker, testdatadir) ->
|
||||
processed=deepcopy(processed),
|
||||
start_date=min_date,
|
||||
end_date=max_date,
|
||||
max_open_trades=10,
|
||||
)
|
||||
results = result['results']
|
||||
assert not results.empty
|
||||
@@ -50,6 +50,7 @@ def test_backtest_position_adjustment(default_conf, fee, mocker, testdatadir) ->
|
||||
expected = pd.DataFrame(
|
||||
{'pair': [pair, pair],
|
||||
'stake_amount': [500.0, 100.0],
|
||||
'max_stake_amount': [500.0, 100],
|
||||
'amount': [4806.87657523, 970.63960782],
|
||||
'open_date': pd.to_datetime([Arrow(2018, 1, 29, 18, 40, 0).datetime,
|
||||
Arrow(2018, 1, 30, 3, 30, 0).datetime], utc=True
|
||||
@@ -98,10 +99,10 @@ def test_backtest_position_adjustment(default_conf, fee, mocker, testdatadir) ->
|
||||
])
|
||||
def test_backtest_position_adjustment_detailed(default_conf, fee, mocker, leverage) -> None:
|
||||
default_conf['use_exit_signal'] = False
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=10)
|
||||
mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf'))
|
||||
mocker.patch("freqtrade.exchange.Exchange.get_max_leverage", return_value=10)
|
||||
mocker.patch(f'{EXMS}.get_fee', fee)
|
||||
mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=10)
|
||||
mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf'))
|
||||
mocker.patch(f"{EXMS}.get_max_leverage", return_value=10)
|
||||
|
||||
patch_exchange(mocker)
|
||||
default_conf.update({
|
||||
|
@@ -6,7 +6,7 @@ from unittest.mock import MagicMock
|
||||
from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_edge
|
||||
from freqtrade.enums import RunMode
|
||||
from freqtrade.optimize.edge_cli import EdgeCli
|
||||
from tests.conftest import (CURRENT_TEST_STRATEGY, get_args, log_has, patch_exchange,
|
||||
from tests.conftest import (CURRENT_TEST_STRATEGY, EXMS, get_args, log_has, patch_exchange,
|
||||
patched_configuration_load_config_file)
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ def test_setup_edge_configuration_with_arguments(mocker, edge_conf, caplog) -> N
|
||||
|
||||
def test_start(mocker, fee, edge_conf, caplog) -> None:
|
||||
start_mock = MagicMock()
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||
mocker.patch(f'{EXMS}.get_fee', fee)
|
||||
patch_exchange(mocker)
|
||||
mocker.patch('freqtrade.optimize.edge_cli.EdgeCli.start', start_mock)
|
||||
patched_configuration_load_config_file(mocker, edge_conf)
|
||||
@@ -101,7 +101,7 @@ def test_edge_init_fee(mocker, edge_conf) -> None:
|
||||
patch_exchange(mocker)
|
||||
edge_conf['fee'] = 0.1234
|
||||
edge_conf['stake_amount'] = 20
|
||||
fee_mock = mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.5))
|
||||
fee_mock = mocker.patch(f'{EXMS}.get_fee', return_value=0.5)
|
||||
edge_cli = EdgeCli(edge_conf)
|
||||
assert edge_cli.edge.fee == 0.1234
|
||||
assert fee_mock.call_count == 0
|
||||
|
@@ -1,5 +1,6 @@
|
||||
# pragma pylint: disable=missing-docstring,W0212,C0103
|
||||
from datetime import datetime, timedelta
|
||||
from functools import wraps
|
||||
from pathlib import Path
|
||||
from unittest.mock import ANY, MagicMock, PropertyMock
|
||||
|
||||
@@ -7,6 +8,7 @@ import pandas as pd
|
||||
import pytest
|
||||
from arrow import Arrow
|
||||
from filelock import Timeout
|
||||
from skopt.space import Integer
|
||||
|
||||
from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_hyperopt
|
||||
from freqtrade.data.history import load_data
|
||||
@@ -18,7 +20,7 @@ from freqtrade.optimize.hyperopt_tools import HyperoptTools
|
||||
from freqtrade.optimize.optimize_reports import generate_strategy_stats
|
||||
from freqtrade.optimize.space import SKDecimal
|
||||
from freqtrade.strategy import IntParameter
|
||||
from tests.conftest import (CURRENT_TEST_STRATEGY, get_args, get_markets, log_has, log_has_re,
|
||||
from tests.conftest import (CURRENT_TEST_STRATEGY, EXMS, get_args, get_markets, log_has, log_has_re,
|
||||
patch_exchange, patched_configuration_load_config_file)
|
||||
|
||||
|
||||
@@ -292,6 +294,8 @@ def test_params_no_optimize_details(hyperopt) -> None:
|
||||
assert res['roi']['0'] == 0.04
|
||||
assert "stoploss" in res
|
||||
assert res['stoploss']['stoploss'] == -0.1
|
||||
assert "max_open_trades" in res
|
||||
assert res['max_open_trades']['max_open_trades'] == 1
|
||||
|
||||
|
||||
def test_start_calls_optimizer(mocker, hyperopt_conf, capsys) -> None:
|
||||
@@ -334,8 +338,7 @@ def test_start_calls_optimizer(mocker, hyperopt_conf, capsys) -> None:
|
||||
assert dumper2.call_count == 1
|
||||
assert hasattr(hyperopt.backtesting.strategy, "advise_exit")
|
||||
assert hasattr(hyperopt.backtesting.strategy, "advise_entry")
|
||||
assert hasattr(hyperopt, "max_open_trades")
|
||||
assert hyperopt.max_open_trades == hyperopt_conf['max_open_trades']
|
||||
assert hyperopt.backtesting.strategy.max_open_trades == hyperopt_conf['max_open_trades']
|
||||
assert hasattr(hyperopt.backtesting, "_position_stacking")
|
||||
|
||||
|
||||
@@ -474,6 +477,7 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None:
|
||||
'trailing_stop_positive': 0.02,
|
||||
'trailing_stop_positive_offset_p1': 0.05,
|
||||
'trailing_only_offset_is_reached': False,
|
||||
'max_open_trades': 3,
|
||||
}
|
||||
response_expected = {
|
||||
'loss': 1.9147239021396234,
|
||||
@@ -499,7 +503,9 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None:
|
||||
'trailing': {'trailing_only_offset_is_reached': False,
|
||||
'trailing_stop': True,
|
||||
'trailing_stop_positive': 0.02,
|
||||
'trailing_stop_positive_offset': 0.07}},
|
||||
'trailing_stop_positive_offset': 0.07},
|
||||
'max_open_trades': {'max_open_trades': 3}
|
||||
},
|
||||
'params_dict': optimizer_param,
|
||||
'params_not_optimized': {'buy': {}, 'protection': {}, 'sell': {}},
|
||||
'results_metrics': ANY,
|
||||
@@ -548,7 +554,8 @@ def test_print_json_spaces_all(mocker, hyperopt_conf, capsys) -> None:
|
||||
'buy': {'mfi-value': None},
|
||||
'sell': {'sell-mfi-value': None},
|
||||
'roi': {}, 'stoploss': {'stoploss': None},
|
||||
'trailing': {'trailing_stop': None}
|
||||
'trailing': {'trailing_stop': None},
|
||||
'max_open_trades': {'max_open_trades': None}
|
||||
},
|
||||
'results_metrics': generate_result_metrics(),
|
||||
}])
|
||||
@@ -571,7 +578,7 @@ def test_print_json_spaces_all(mocker, hyperopt_conf, capsys) -> None:
|
||||
out, err = capsys.readouterr()
|
||||
result_str = (
|
||||
'{"params":{"mfi-value":null,"sell-mfi-value":null},"minimal_roi"'
|
||||
':{},"stoploss":null,"trailing_stop":null}'
|
||||
':{},"stoploss":null,"trailing_stop":null,"max_open_trades":null}'
|
||||
)
|
||||
assert result_str in out # noqa: E501
|
||||
# Should be called for historical candle data
|
||||
@@ -702,8 +709,7 @@ def test_simplified_interface_roi_stoploss(mocker, hyperopt_conf, capsys) -> Non
|
||||
|
||||
assert hasattr(hyperopt.backtesting.strategy, "advise_exit")
|
||||
assert hasattr(hyperopt.backtesting.strategy, "advise_entry")
|
||||
assert hasattr(hyperopt, "max_open_trades")
|
||||
assert hyperopt.max_open_trades == hyperopt_conf['max_open_trades']
|
||||
assert hyperopt.backtesting.strategy.max_open_trades == hyperopt_conf['max_open_trades']
|
||||
assert hasattr(hyperopt.backtesting, "_position_stacking")
|
||||
|
||||
|
||||
@@ -776,8 +782,7 @@ def test_simplified_interface_buy(mocker, hyperopt_conf, capsys) -> None:
|
||||
assert dumper2.call_count == 1
|
||||
assert hasattr(hyperopt.backtesting.strategy, "advise_exit")
|
||||
assert hasattr(hyperopt.backtesting.strategy, "advise_entry")
|
||||
assert hasattr(hyperopt, "max_open_trades")
|
||||
assert hyperopt.max_open_trades == hyperopt_conf['max_open_trades']
|
||||
assert hyperopt.backtesting.strategy.max_open_trades == hyperopt_conf['max_open_trades']
|
||||
assert hasattr(hyperopt.backtesting, "_position_stacking")
|
||||
|
||||
|
||||
@@ -819,8 +824,7 @@ def test_simplified_interface_sell(mocker, hyperopt_conf, capsys) -> None:
|
||||
assert dumper2.call_count == 1
|
||||
assert hasattr(hyperopt.backtesting.strategy, "advise_exit")
|
||||
assert hasattr(hyperopt.backtesting.strategy, "advise_entry")
|
||||
assert hasattr(hyperopt, "max_open_trades")
|
||||
assert hyperopt.max_open_trades == hyperopt_conf['max_open_trades']
|
||||
assert hyperopt.backtesting.strategy.max_open_trades == hyperopt_conf['max_open_trades']
|
||||
assert hasattr(hyperopt.backtesting, "_position_stacking")
|
||||
|
||||
|
||||
@@ -855,7 +859,7 @@ def test_simplified_interface_failed(mocker, hyperopt_conf, space) -> None:
|
||||
|
||||
def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir, fee) -> None:
|
||||
patch_exchange(mocker)
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||
mocker.patch(f'{EXMS}.get_fee', fee)
|
||||
(Path(tmpdir) / 'hyperopt_results').mkdir(parents=True)
|
||||
# No hyperopt needed
|
||||
hyperopt_conf.update({
|
||||
@@ -874,6 +878,7 @@ def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir, fee) -> None:
|
||||
assert hyperopt.backtesting.strategy.buy_rsi.value == 35
|
||||
assert hyperopt.backtesting.strategy.sell_rsi.value == 74
|
||||
assert hyperopt.backtesting.strategy.protection_cooldown_lookback.value == 30
|
||||
assert hyperopt.backtesting.strategy.max_open_trades == 1
|
||||
buy_rsi_range = hyperopt.backtesting.strategy.buy_rsi.range
|
||||
assert isinstance(buy_rsi_range, range)
|
||||
# Range from 0 - 50 (inclusive)
|
||||
@@ -884,6 +889,7 @@ def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir, fee) -> None:
|
||||
assert hyperopt.backtesting.strategy.protection_cooldown_lookback.value != 30
|
||||
assert hyperopt.backtesting.strategy.buy_rsi.value != 35
|
||||
assert hyperopt.backtesting.strategy.sell_rsi.value != 74
|
||||
assert hyperopt.backtesting.strategy.max_open_trades != 1
|
||||
|
||||
hyperopt.custom_hyperopt.generate_estimator = lambda *args, **kwargs: 'ET1'
|
||||
with pytest.raises(OperationalException, match="Estimator ET1 not supported."):
|
||||
@@ -891,10 +897,10 @@ def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir, fee) -> None:
|
||||
|
||||
|
||||
def test_in_strategy_auto_hyperopt_with_parallel(mocker, hyperopt_conf, tmpdir, fee) -> None:
|
||||
mocker.patch('freqtrade.exchange.Exchange.validate_config', MagicMock())
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||
mocker.patch('freqtrade.exchange.Exchange._load_markets')
|
||||
mocker.patch('freqtrade.exchange.Exchange.markets',
|
||||
mocker.patch(f'{EXMS}.validate_config', MagicMock())
|
||||
mocker.patch(f'{EXMS}.get_fee', fee)
|
||||
mocker.patch(f'{EXMS}._load_markets')
|
||||
mocker.patch(f'{EXMS}.markets',
|
||||
PropertyMock(return_value=get_markets()))
|
||||
(Path(tmpdir) / 'hyperopt_results').mkdir(parents=True)
|
||||
# No hyperopt needed
|
||||
@@ -932,7 +938,7 @@ def test_in_strategy_auto_hyperopt_with_parallel(mocker, hyperopt_conf, tmpdir,
|
||||
|
||||
def test_in_strategy_auto_hyperopt_per_epoch(mocker, hyperopt_conf, tmpdir, fee) -> None:
|
||||
patch_exchange(mocker)
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||
mocker.patch(f'{EXMS}.get_fee', fee)
|
||||
(Path(tmpdir) / 'hyperopt_results').mkdir(parents=True)
|
||||
|
||||
hyperopt_conf.update({
|
||||
@@ -984,3 +990,124 @@ def test_SKDecimal():
|
||||
assert space.transform([2.0]) == [200]
|
||||
assert space.transform([1.0]) == [100]
|
||||
assert space.transform([1.5, 1.6]) == [150, 160]
|
||||
|
||||
|
||||
def test_stake_amount_unlimited_max_open_trades(mocker, hyperopt_conf, tmpdir, fee) -> None:
|
||||
# This test is to ensure that unlimited max_open_trades are ignored for the backtesting
|
||||
# if we have an unlimited stake amount
|
||||
patch_exchange(mocker)
|
||||
mocker.patch(f'{EXMS}.get_fee', fee)
|
||||
(Path(tmpdir) / 'hyperopt_results').mkdir(parents=True)
|
||||
hyperopt_conf.update({
|
||||
'strategy': 'HyperoptableStrategy',
|
||||
'user_data_dir': Path(tmpdir),
|
||||
'hyperopt_random_state': 42,
|
||||
'spaces': ['trades'],
|
||||
'stake_amount': 'unlimited'
|
||||
})
|
||||
hyperopt = Hyperopt(hyperopt_conf)
|
||||
mocker.patch('freqtrade.optimize.hyperopt.Hyperopt._get_params_dict',
|
||||
return_value={
|
||||
'max_open_trades': -1
|
||||
})
|
||||
|
||||
assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto)
|
||||
|
||||
assert hyperopt.backtesting.strategy.max_open_trades == 1
|
||||
|
||||
hyperopt.start()
|
||||
|
||||
assert hyperopt.backtesting.strategy.max_open_trades == 1
|
||||
|
||||
|
||||
def test_max_open_trades_dump(mocker, hyperopt_conf, tmpdir, fee, capsys) -> None:
|
||||
# This test is to ensure that after hyperopting, max_open_trades is never
|
||||
# saved as inf in the output json params
|
||||
patch_exchange(mocker)
|
||||
mocker.patch(f'{EXMS}.get_fee', fee)
|
||||
(Path(tmpdir) / 'hyperopt_results').mkdir(parents=True)
|
||||
hyperopt_conf.update({
|
||||
'strategy': 'HyperoptableStrategy',
|
||||
'user_data_dir': Path(tmpdir),
|
||||
'hyperopt_random_state': 42,
|
||||
'spaces': ['trades'],
|
||||
})
|
||||
hyperopt = Hyperopt(hyperopt_conf)
|
||||
mocker.patch('freqtrade.optimize.hyperopt.Hyperopt._get_params_dict',
|
||||
return_value={
|
||||
'max_open_trades': -1
|
||||
})
|
||||
|
||||
assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto)
|
||||
|
||||
hyperopt.start()
|
||||
|
||||
out, err = capsys.readouterr()
|
||||
|
||||
assert 'max_open_trades = -1' in out
|
||||
assert 'max_open_trades = inf' not in out
|
||||
|
||||
##############
|
||||
|
||||
hyperopt_conf.update({'print_json': True})
|
||||
|
||||
hyperopt = Hyperopt(hyperopt_conf)
|
||||
mocker.patch('freqtrade.optimize.hyperopt.Hyperopt._get_params_dict',
|
||||
return_value={
|
||||
'max_open_trades': -1
|
||||
})
|
||||
|
||||
assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto)
|
||||
|
||||
hyperopt.start()
|
||||
|
||||
out, err = capsys.readouterr()
|
||||
|
||||
assert '"max_open_trades":-1' in out
|
||||
|
||||
|
||||
def test_max_open_trades_consistency(mocker, hyperopt_conf, tmpdir, fee) -> None:
|
||||
# This test is to ensure that max_open_trades is the same across all functions needing it
|
||||
# after it has been changed from the hyperopt
|
||||
patch_exchange(mocker)
|
||||
mocker.patch(f'{EXMS}.get_fee', return_value=0)
|
||||
|
||||
(Path(tmpdir) / 'hyperopt_results').mkdir(parents=True)
|
||||
hyperopt_conf.update({
|
||||
'strategy': 'HyperoptableStrategy',
|
||||
'user_data_dir': Path(tmpdir),
|
||||
'hyperopt_random_state': 42,
|
||||
'spaces': ['trades'],
|
||||
'stake_amount': 'unlimited',
|
||||
'dry_run_wallet': 8,
|
||||
'available_capital': 8,
|
||||
'dry_run': True,
|
||||
'epochs': 1
|
||||
})
|
||||
hyperopt = Hyperopt(hyperopt_conf)
|
||||
|
||||
assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto)
|
||||
|
||||
hyperopt.custom_hyperopt.max_open_trades_space = lambda: [
|
||||
Integer(1, 10, name='max_open_trades')]
|
||||
|
||||
first_time_evaluated = False
|
||||
|
||||
def stake_amount_interceptor(func):
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
nonlocal first_time_evaluated
|
||||
stake_amount = func(*args, **kwargs)
|
||||
if first_time_evaluated is False:
|
||||
assert stake_amount == 1
|
||||
first_time_evaluated = True
|
||||
return stake_amount
|
||||
return wrapper
|
||||
|
||||
hyperopt.backtesting.wallets._calculate_unlimited_stake_amount = stake_amount_interceptor(
|
||||
hyperopt.backtesting.wallets._calculate_unlimited_stake_amount)
|
||||
|
||||
hyperopt.start()
|
||||
|
||||
assert hyperopt.backtesting.strategy.max_open_trades == 8
|
||||
assert hyperopt.config['max_open_trades'] == 8
|
||||
|
@@ -66,52 +66,58 @@ def test_load_previous_results2(mocker, testdatadir, caplog) -> None:
|
||||
@pytest.mark.parametrize("spaces, expected_results", [
|
||||
(['buy'],
|
||||
{'buy': True, 'sell': False, 'roi': False, 'stoploss': False, 'trailing': False,
|
||||
'protection': False}),
|
||||
'protection': False, 'trades': False}),
|
||||
(['sell'],
|
||||
{'buy': False, 'sell': True, 'roi': False, 'stoploss': False, 'trailing': False,
|
||||
'protection': False}),
|
||||
'protection': False, 'trades': False}),
|
||||
(['roi'],
|
||||
{'buy': False, 'sell': False, 'roi': True, 'stoploss': False, 'trailing': False,
|
||||
'protection': False}),
|
||||
'protection': False, 'trades': False}),
|
||||
(['stoploss'],
|
||||
{'buy': False, 'sell': False, 'roi': False, 'stoploss': True, 'trailing': False,
|
||||
'protection': False}),
|
||||
'protection': False, 'trades': False}),
|
||||
(['trailing'],
|
||||
{'buy': False, 'sell': False, 'roi': False, 'stoploss': False, 'trailing': True,
|
||||
'protection': False}),
|
||||
'protection': False, 'trades': False}),
|
||||
(['buy', 'sell', 'roi', 'stoploss'],
|
||||
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': False,
|
||||
'protection': False}),
|
||||
'protection': False, 'trades': False}),
|
||||
(['buy', 'sell', 'roi', 'stoploss', 'trailing'],
|
||||
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True,
|
||||
'protection': False}),
|
||||
'protection': False, 'trades': False}),
|
||||
(['buy', 'roi'],
|
||||
{'buy': True, 'sell': False, 'roi': True, 'stoploss': False, 'trailing': False,
|
||||
'protection': False}),
|
||||
'protection': False, 'trades': False}),
|
||||
(['all'],
|
||||
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True,
|
||||
'protection': True}),
|
||||
'protection': True, 'trades': True}),
|
||||
(['default'],
|
||||
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': False,
|
||||
'protection': False}),
|
||||
'protection': False, 'trades': False}),
|
||||
(['default', 'trailing'],
|
||||
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True,
|
||||
'protection': False}),
|
||||
'protection': False, 'trades': False}),
|
||||
(['all', 'buy'],
|
||||
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True,
|
||||
'protection': True}),
|
||||
'protection': True, 'trades': True}),
|
||||
(['default', 'buy'],
|
||||
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': False,
|
||||
'protection': False}),
|
||||
'protection': False, 'trades': False}),
|
||||
(['all'],
|
||||
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True,
|
||||
'protection': True}),
|
||||
'protection': True, 'trades': True}),
|
||||
(['protection'],
|
||||
{'buy': False, 'sell': False, 'roi': False, 'stoploss': False, 'trailing': False,
|
||||
'protection': True}),
|
||||
'protection': True, 'trades': False}),
|
||||
(['trades'],
|
||||
{'buy': False, 'sell': False, 'roi': False, 'stoploss': False, 'trailing': False,
|
||||
'protection': False, 'trades': True}),
|
||||
(['default', 'trades'],
|
||||
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': False,
|
||||
'protection': False, 'trades': True}),
|
||||
])
|
||||
def test_has_space(hyperopt_conf, spaces, expected_results):
|
||||
for s in ['buy', 'sell', 'roi', 'stoploss', 'trailing', 'protection']:
|
||||
for s in ['buy', 'sell', 'roi', 'stoploss', 'trailing', 'protection', 'trades']:
|
||||
hyperopt_conf.update({'spaces': spaces})
|
||||
assert HyperoptTools.has_space(hyperopt_conf, s) == expected_results[s]
|
||||
|
||||
@@ -193,6 +199,9 @@ def test_export_params(tmpdir):
|
||||
"346": 0.08499,
|
||||
"507": 0.049,
|
||||
"1595": 0
|
||||
},
|
||||
"max_open_trades": {
|
||||
"max_open_trades": 5
|
||||
}
|
||||
},
|
||||
"params_not_optimized": {
|
||||
@@ -219,6 +228,7 @@ def test_export_params(tmpdir):
|
||||
assert "roi" in content["params"]
|
||||
assert "stoploss" in content["params"]
|
||||
assert "trailing" in content["params"]
|
||||
assert "max_open_trades" in content["params"]
|
||||
|
||||
|
||||
def test_try_export_params(default_conf, tmpdir, caplog, mocker):
|
||||
@@ -297,6 +307,9 @@ def test_params_print(capsys):
|
||||
"trailing_stop_positive_offset": 0.1,
|
||||
"trailing_only_offset_is_reached": True
|
||||
},
|
||||
"max_open_trades": {
|
||||
"max_open_trades": 5
|
||||
}
|
||||
|
||||
}
|
||||
HyperoptTools._params_pretty_print(params, 'buy', 'No header', non_optimized)
|
||||
@@ -327,6 +340,13 @@ def test_params_print(capsys):
|
||||
assert re.search('trailing_stop_positive_offset = 0.1 # value loaded.*\n', captured.out)
|
||||
assert re.search('trailing_only_offset_is_reached = True # value loaded.*\n', captured.out)
|
||||
|
||||
HyperoptTools._params_pretty_print(
|
||||
params, 'max_open_trades', "Max Open Trades:", non_optimized)
|
||||
captured = capsys.readouterr()
|
||||
|
||||
assert re.search("# Max Open Trades:", captured.out)
|
||||
assert re.search('max_open_trades = 5 # value loaded.*\n', captured.out)
|
||||
|
||||
|
||||
def test_hyperopt_serializer():
|
||||
|
||||
|
@@ -257,9 +257,8 @@ def test_write_read_backtest_candles(tmpdir):
|
||||
sample_date = '2022_01_01_15_05_13'
|
||||
store_backtest_analysis_results(Path(tmpdir), candle_dict, {}, sample_date)
|
||||
stored_file = Path(tmpdir / f'backtest-result-{sample_date}_signals.pkl')
|
||||
scp = open(stored_file, "rb")
|
||||
pickled_signal_candles = joblib.load(scp)
|
||||
scp.close()
|
||||
with stored_file.open("rb") as scp:
|
||||
pickled_signal_candles = joblib.load(scp)
|
||||
|
||||
assert pickled_signal_candles.keys() == candle_dict.keys()
|
||||
assert pickled_signal_candles['DefStrat'].keys() == pickled_signal_candles['DefStrat'].keys()
|
||||
@@ -272,9 +271,8 @@ def test_write_read_backtest_candles(tmpdir):
|
||||
filename = Path(tmpdir / 'testresult')
|
||||
store_backtest_analysis_results(filename, candle_dict, {}, sample_date)
|
||||
stored_file = Path(tmpdir / f'testresult-{sample_date}_signals.pkl')
|
||||
scp = open(stored_file, "rb")
|
||||
pickled_signal_candles = joblib.load(scp)
|
||||
scp.close()
|
||||
with stored_file.open("rb") as scp:
|
||||
pickled_signal_candles = joblib.load(scp)
|
||||
|
||||
assert pickled_signal_candles.keys() == candle_dict.keys()
|
||||
assert pickled_signal_candles['DefStrat'].keys() == pickled_signal_candles['DefStrat'].keys()
|
||||
@@ -311,7 +309,7 @@ def test_generate_pair_metrics():
|
||||
|
||||
def test_generate_daily_stats(testdatadir):
|
||||
|
||||
filename = testdatadir / "backtest_results/backtest-result_new.json"
|
||||
filename = testdatadir / "backtest_results/backtest-result.json"
|
||||
bt_data = load_backtest_data(filename)
|
||||
res = generate_daily_stats(bt_data)
|
||||
assert isinstance(res, dict)
|
||||
@@ -331,7 +329,7 @@ def test_generate_daily_stats(testdatadir):
|
||||
|
||||
|
||||
def test_generate_trading_stats(testdatadir):
|
||||
filename = testdatadir / "backtest_results/backtest-result_new.json"
|
||||
filename = testdatadir / "backtest_results/backtest-result.json"
|
||||
bt_data = load_backtest_data(filename)
|
||||
res = generate_trading_stats(bt_data)
|
||||
assert isinstance(res, dict)
|
||||
@@ -447,7 +445,7 @@ def test_generate_edge_table():
|
||||
|
||||
|
||||
def test_generate_periodic_breakdown_stats(testdatadir):
|
||||
filename = testdatadir / "backtest_results/backtest-result_new.json"
|
||||
filename = testdatadir / "backtest_results/backtest-result.json"
|
||||
bt_data = load_backtest_data(filename).to_dict(orient='records')
|
||||
|
||||
res = generate_periodic_breakdown_stats(bt_data, 'day')
|
||||
@@ -475,7 +473,7 @@ def test__get_resample_from_period():
|
||||
|
||||
|
||||
def test_show_sorted_pairlist(testdatadir, default_conf, capsys):
|
||||
filename = testdatadir / "backtest_results/backtest-result_new.json"
|
||||
filename = testdatadir / "backtest_results/backtest-result.json"
|
||||
bt_data = load_backtest_stats(filename)
|
||||
default_conf['backtest_show_pair_list'] = True
|
||||
|
||||
|
413
tests/persistence/test_migrations.py
Normal file
413
tests/persistence/test_migrations.py
Normal file
@@ -0,0 +1,413 @@
|
||||
# pragma pylint: disable=missing-docstring, C0103
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import create_engine, select, text
|
||||
|
||||
from freqtrade.constants import DEFAULT_DB_PROD_URL
|
||||
from freqtrade.enums import TradingMode
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.persistence import Trade, init_db
|
||||
from freqtrade.persistence.migrations import get_last_sequence_ids, set_sequence_ids
|
||||
from freqtrade.persistence.models import PairLock
|
||||
from tests.conftest import log_has
|
||||
|
||||
|
||||
spot, margin, futures = TradingMode.SPOT, TradingMode.MARGIN, TradingMode.FUTURES
|
||||
|
||||
|
||||
def test_init_create_session(default_conf):
|
||||
# Check if init create a session
|
||||
init_db(default_conf['db_url'])
|
||||
assert hasattr(Trade, 'session')
|
||||
assert 'scoped_session' in type(Trade.session).__name__
|
||||
|
||||
|
||||
def test_init_custom_db_url(default_conf, tmpdir):
|
||||
# Update path to a value other than default, but still in-memory
|
||||
filename = f"{tmpdir}/freqtrade2_test.sqlite"
|
||||
assert not Path(filename).is_file()
|
||||
|
||||
default_conf.update({'db_url': f'sqlite:///{filename}'})
|
||||
|
||||
init_db(default_conf['db_url'])
|
||||
assert Path(filename).is_file()
|
||||
r = Trade.session.execute(text("PRAGMA journal_mode"))
|
||||
assert r.first() == ('wal',)
|
||||
|
||||
|
||||
def test_init_invalid_db_url():
|
||||
# Update path to a value other than default, but still in-memory
|
||||
with pytest.raises(OperationalException, match=r'.*no valid database URL*'):
|
||||
init_db('unknown:///some.url')
|
||||
|
||||
with pytest.raises(OperationalException, match=r'Bad db-url.*For in-memory database, pl.*'):
|
||||
init_db('sqlite:///')
|
||||
|
||||
|
||||
def test_init_prod_db(default_conf, mocker):
|
||||
default_conf.update({'dry_run': False})
|
||||
default_conf.update({'db_url': DEFAULT_DB_PROD_URL})
|
||||
|
||||
create_engine_mock = mocker.patch('freqtrade.persistence.models.create_engine', MagicMock())
|
||||
|
||||
init_db(default_conf['db_url'])
|
||||
assert create_engine_mock.call_count == 1
|
||||
assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tradesv3.sqlite'
|
||||
|
||||
|
||||
def test_init_dryrun_db(default_conf, tmpdir):
|
||||
filename = f"{tmpdir}/freqtrade2_prod.sqlite"
|
||||
assert not Path(filename).is_file()
|
||||
default_conf.update({
|
||||
'dry_run': True,
|
||||
'db_url': f'sqlite:///{filename}'
|
||||
})
|
||||
|
||||
init_db(default_conf['db_url'])
|
||||
assert Path(filename).is_file()
|
||||
|
||||
|
||||
def test_migrate_new(mocker, default_conf, fee, caplog):
|
||||
"""
|
||||
Test Database migration (starting with new pairformat)
|
||||
"""
|
||||
caplog.set_level(logging.DEBUG)
|
||||
amount = 103.223
|
||||
# Always create all columns apart from the last!
|
||||
create_table_old = """CREATE TABLE IF NOT EXISTS "trades" (
|
||||
id INTEGER NOT NULL,
|
||||
exchange VARCHAR NOT NULL,
|
||||
pair VARCHAR NOT NULL,
|
||||
is_open BOOLEAN NOT NULL,
|
||||
fee FLOAT NOT NULL,
|
||||
open_rate FLOAT,
|
||||
close_rate FLOAT,
|
||||
close_profit FLOAT,
|
||||
stake_amount FLOAT NOT NULL,
|
||||
amount FLOAT,
|
||||
open_date DATETIME NOT NULL,
|
||||
close_date DATETIME,
|
||||
open_order_id VARCHAR,
|
||||
stop_loss FLOAT,
|
||||
initial_stop_loss FLOAT,
|
||||
max_rate FLOAT,
|
||||
sell_reason VARCHAR,
|
||||
strategy VARCHAR,
|
||||
ticker_interval INTEGER,
|
||||
stoploss_order_id VARCHAR,
|
||||
PRIMARY KEY (id),
|
||||
CHECK (is_open IN (0, 1))
|
||||
);"""
|
||||
create_table_order = """CREATE TABLE orders (
|
||||
id INTEGER NOT NULL,
|
||||
ft_trade_id INTEGER,
|
||||
ft_order_side VARCHAR(25) NOT NULL,
|
||||
ft_pair VARCHAR(25) NOT NULL,
|
||||
ft_is_open BOOLEAN NOT NULL,
|
||||
order_id VARCHAR(255) NOT NULL,
|
||||
status VARCHAR(255),
|
||||
symbol VARCHAR(25),
|
||||
order_type VARCHAR(50),
|
||||
side VARCHAR(25),
|
||||
price FLOAT,
|
||||
amount FLOAT,
|
||||
filled FLOAT,
|
||||
remaining FLOAT,
|
||||
cost FLOAT,
|
||||
order_date DATETIME,
|
||||
order_filled_date DATETIME,
|
||||
order_update_date DATETIME,
|
||||
PRIMARY KEY (id)
|
||||
);"""
|
||||
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, ticker_interval,
|
||||
open_order_id, stoploss_order_id)
|
||||
VALUES ('binance', 'ETC/BTC', 1, {fee},
|
||||
0.00258580, {stake}, {amount},
|
||||
'2019-11-28 12:44:24.000000',
|
||||
0.0, 0.0, 0.0, '5m',
|
||||
'buy_order', 'dry_stop_order_id222')
|
||||
""".format(fee=fee.return_value,
|
||||
stake=default_conf.get("stake_amount"),
|
||||
amount=amount
|
||||
)
|
||||
insert_orders = f"""
|
||||
insert into orders (
|
||||
ft_trade_id,
|
||||
ft_order_side,
|
||||
ft_pair,
|
||||
ft_is_open,
|
||||
order_id,
|
||||
status,
|
||||
symbol,
|
||||
order_type,
|
||||
side,
|
||||
price,
|
||||
amount,
|
||||
filled,
|
||||
remaining,
|
||||
cost)
|
||||
values (
|
||||
1,
|
||||
'buy',
|
||||
'ETC/BTC',
|
||||
0,
|
||||
'dry_buy_order',
|
||||
'closed',
|
||||
'ETC/BTC',
|
||||
'limit',
|
||||
'buy',
|
||||
0.00258580,
|
||||
{amount},
|
||||
{amount},
|
||||
0,
|
||||
{amount * 0.00258580}
|
||||
),
|
||||
(
|
||||
1,
|
||||
'buy',
|
||||
'ETC/BTC',
|
||||
1,
|
||||
'dry_buy_order22',
|
||||
'canceled',
|
||||
'ETC/BTC',
|
||||
'limit',
|
||||
'buy',
|
||||
0.00258580,
|
||||
{amount},
|
||||
{amount},
|
||||
0,
|
||||
{amount * 0.00258580}
|
||||
),
|
||||
(
|
||||
1,
|
||||
'stoploss',
|
||||
'ETC/BTC',
|
||||
1,
|
||||
'dry_stop_order_id11X',
|
||||
'canceled',
|
||||
'ETC/BTC',
|
||||
'limit',
|
||||
'sell',
|
||||
0.00258580,
|
||||
{amount},
|
||||
{amount},
|
||||
0,
|
||||
{amount * 0.00258580}
|
||||
),
|
||||
(
|
||||
1,
|
||||
'stoploss',
|
||||
'ETC/BTC',
|
||||
1,
|
||||
'dry_stop_order_id222',
|
||||
'open',
|
||||
'ETC/BTC',
|
||||
'limit',
|
||||
'sell',
|
||||
0.00258580,
|
||||
{amount},
|
||||
{amount},
|
||||
0,
|
||||
{amount * 0.00258580}
|
||||
)
|
||||
"""
|
||||
engine = create_engine('sqlite://')
|
||||
mocker.patch('freqtrade.persistence.models.create_engine', lambda *args, **kwargs: engine)
|
||||
|
||||
# Create table using the old format
|
||||
with engine.begin() as connection:
|
||||
connection.execute(text(create_table_old))
|
||||
connection.execute(text(create_table_order))
|
||||
connection.execute(text("create index ix_trades_is_open on trades(is_open)"))
|
||||
connection.execute(text("create index ix_trades_pair on trades(pair)"))
|
||||
connection.execute(text(insert_table_old))
|
||||
connection.execute(text(insert_orders))
|
||||
|
||||
# fake previous backup
|
||||
connection.execute(text("create table trades_bak as select * from trades"))
|
||||
|
||||
connection.execute(text("create table trades_bak1 as select * from trades"))
|
||||
# Run init to test migration
|
||||
init_db(default_conf['db_url'])
|
||||
|
||||
trades = Trade.session.scalars(select(Trade).filter(Trade.id == 1)).all()
|
||||
assert len(trades) == 1
|
||||
trade = trades[0]
|
||||
assert trade.fee_open == fee.return_value
|
||||
assert trade.fee_close == fee.return_value
|
||||
assert trade.open_rate_requested is None
|
||||
assert trade.close_rate_requested is None
|
||||
assert trade.is_open == 1
|
||||
assert trade.amount == amount
|
||||
assert trade.amount_requested == amount
|
||||
assert trade.stake_amount == default_conf.get("stake_amount")
|
||||
assert trade.pair == "ETC/BTC"
|
||||
assert trade.exchange == "binance"
|
||||
assert trade.max_rate == 0.0
|
||||
assert trade.min_rate is None
|
||||
assert trade.stop_loss == 0.0
|
||||
assert trade.initial_stop_loss == 0.0
|
||||
assert trade.exit_reason is None
|
||||
assert trade.strategy is None
|
||||
assert trade.timeframe == '5m'
|
||||
assert trade.stoploss_order_id == 'dry_stop_order_id222'
|
||||
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 for trades - backup: trades_bak2, orders_bak0",
|
||||
caplog)
|
||||
assert log_has("Database migration finished.", caplog)
|
||||
assert pytest.approx(trade.open_trade_value) == trade._calc_open_trade_value(
|
||||
trade.amount, trade.open_rate)
|
||||
assert trade.close_profit_abs is None
|
||||
assert trade.stake_amount == trade.max_stake_amount
|
||||
|
||||
orders = trade.orders
|
||||
assert len(orders) == 4
|
||||
assert orders[0].order_id == 'dry_buy_order'
|
||||
assert orders[0].ft_order_side == 'buy'
|
||||
|
||||
assert orders[-1].order_id == 'dry_stop_order_id222'
|
||||
assert orders[-1].ft_order_side == 'stoploss'
|
||||
assert orders[-1].ft_is_open is True
|
||||
|
||||
assert orders[1].order_id == 'dry_buy_order22'
|
||||
assert orders[1].ft_order_side == 'buy'
|
||||
assert orders[1].ft_is_open is False
|
||||
|
||||
assert orders[2].order_id == 'dry_stop_order_id11X'
|
||||
assert orders[2].ft_order_side == 'stoploss'
|
||||
assert orders[2].ft_is_open is False
|
||||
|
||||
|
||||
def test_migrate_too_old(mocker, default_conf, fee, caplog):
|
||||
"""
|
||||
Test Database migration (starting with new pairformat)
|
||||
"""
|
||||
caplog.set_level(logging.DEBUG)
|
||||
amount = 103.223
|
||||
create_table_old = """CREATE TABLE IF NOT EXISTS "trades" (
|
||||
id INTEGER NOT NULL,
|
||||
exchange VARCHAR NOT NULL,
|
||||
pair VARCHAR NOT NULL,
|
||||
is_open BOOLEAN NOT NULL,
|
||||
fee_open FLOAT NOT NULL,
|
||||
fee_close FLOAT NOT NULL,
|
||||
open_rate FLOAT,
|
||||
close_rate FLOAT,
|
||||
close_profit FLOAT,
|
||||
stake_amount FLOAT NOT NULL,
|
||||
amount FLOAT,
|
||||
open_date DATETIME NOT NULL,
|
||||
close_date DATETIME,
|
||||
open_order_id VARCHAR,
|
||||
PRIMARY KEY (id),
|
||||
CHECK (is_open IN (0, 1))
|
||||
);"""
|
||||
|
||||
insert_table_old = """INSERT INTO trades (exchange, pair, is_open, fee_open, fee_close,
|
||||
open_rate, stake_amount, amount, open_date)
|
||||
VALUES ('binance', 'ETC/BTC', 1, {fee}, {fee},
|
||||
0.00258580, {stake}, {amount},
|
||||
'2019-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.models.create_engine', lambda *args, **kwargs: engine)
|
||||
|
||||
# Create table using the old format
|
||||
with engine.begin() as connection:
|
||||
connection.execute(text(create_table_old))
|
||||
connection.execute(text(insert_table_old))
|
||||
|
||||
# Run init to test migration
|
||||
with pytest.raises(OperationalException, match=r'Your database seems to be very old'):
|
||||
init_db(default_conf['db_url'])
|
||||
|
||||
|
||||
def test_migrate_get_last_sequence_ids():
|
||||
engine = MagicMock()
|
||||
engine.begin = MagicMock()
|
||||
engine.name = 'postgresql'
|
||||
get_last_sequence_ids(engine, 'trades_bak', 'orders_bak')
|
||||
|
||||
assert engine.begin.call_count == 2
|
||||
engine.reset_mock()
|
||||
engine.begin.reset_mock()
|
||||
|
||||
engine.name = 'somethingelse'
|
||||
get_last_sequence_ids(engine, 'trades_bak', 'orders_bak')
|
||||
|
||||
assert engine.begin.call_count == 0
|
||||
|
||||
|
||||
def test_migrate_set_sequence_ids():
|
||||
engine = MagicMock()
|
||||
engine.begin = MagicMock()
|
||||
engine.name = 'postgresql'
|
||||
set_sequence_ids(engine, 22, 55, 5)
|
||||
|
||||
assert engine.begin.call_count == 1
|
||||
engine.reset_mock()
|
||||
engine.begin.reset_mock()
|
||||
|
||||
engine.name = 'somethingelse'
|
||||
set_sequence_ids(engine, 22, 55, 6)
|
||||
|
||||
assert engine.begin.call_count == 0
|
||||
|
||||
|
||||
def test_migrate_pairlocks(mocker, default_conf, fee, caplog):
|
||||
"""
|
||||
Test Database migration (starting with new pairformat)
|
||||
"""
|
||||
caplog.set_level(logging.DEBUG)
|
||||
# Always create all columns apart from the last!
|
||||
create_table_old = """CREATE TABLE pairlocks (
|
||||
id INTEGER NOT NULL,
|
||||
pair VARCHAR(25) NOT NULL,
|
||||
reason VARCHAR(255),
|
||||
lock_time DATETIME NOT NULL,
|
||||
lock_end_time DATETIME NOT NULL,
|
||||
active BOOLEAN NOT NULL,
|
||||
PRIMARY KEY (id)
|
||||
)
|
||||
"""
|
||||
create_index1 = "CREATE INDEX ix_pairlocks_pair ON pairlocks (pair)"
|
||||
create_index2 = "CREATE INDEX ix_pairlocks_lock_end_time ON pairlocks (lock_end_time)"
|
||||
create_index3 = "CREATE INDEX ix_pairlocks_active ON pairlocks (active)"
|
||||
insert_table_old = """INSERT INTO pairlocks (
|
||||
id, pair, reason, lock_time, lock_end_time, active)
|
||||
VALUES (1, 'ETH/BTC', 'Auto lock', '2021-07-12 18:41:03', '2021-07-11 18:45:00', 1)
|
||||
"""
|
||||
insert_table_old2 = """INSERT INTO pairlocks (
|
||||
id, pair, reason, lock_time, lock_end_time, active)
|
||||
VALUES (2, '*', 'Lock all', '2021-07-12 18:41:03', '2021-07-12 19:00:00', 1)
|
||||
"""
|
||||
engine = create_engine('sqlite://')
|
||||
mocker.patch('freqtrade.persistence.models.create_engine', lambda *args, **kwargs: engine)
|
||||
# Create table using the old format
|
||||
with engine.begin() as connection:
|
||||
connection.execute(text(create_table_old))
|
||||
|
||||
connection.execute(text(insert_table_old))
|
||||
connection.execute(text(insert_table_old2))
|
||||
connection.execute(text(create_index1))
|
||||
connection.execute(text(create_index2))
|
||||
connection.execute(text(create_index3))
|
||||
|
||||
init_db(default_conf['db_url'])
|
||||
|
||||
assert len(PairLock.get_all_locks().all()) == 2
|
||||
assert len(PairLock.session.scalars(select(PairLock).filter(PairLock.pair == '*')).all()) == 1
|
||||
pairlocks = PairLock.session.scalars(select(PairLock).filter(PairLock.pair == 'ETH/BTC')).all()
|
||||
assert len(pairlocks) == 1
|
||||
pairlocks[0].pair == 'ETH/BTC'
|
||||
pairlocks[0].side == '*'
|
@@ -1,78 +1,21 @@
|
||||
# pragma pylint: disable=missing-docstring, C0103
|
||||
import logging
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
from types import FunctionType
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import arrow
|
||||
import pytest
|
||||
from sqlalchemy import create_engine, text
|
||||
from sqlalchemy import select
|
||||
|
||||
from freqtrade.constants import DATETIME_PRINT_FORMAT, DEFAULT_DB_PROD_URL
|
||||
from freqtrade.constants import DATETIME_PRINT_FORMAT
|
||||
from freqtrade.enums import TradingMode
|
||||
from freqtrade.exceptions import DependencyException, OperationalException
|
||||
from freqtrade.exceptions import DependencyException
|
||||
from freqtrade.persistence import LocalTrade, Order, Trade, init_db
|
||||
from freqtrade.persistence.migrations import get_last_sequence_ids, set_sequence_ids
|
||||
from freqtrade.persistence.models import PairLock
|
||||
from tests.conftest import create_mock_trades, create_mock_trades_with_leverage, log_has, log_has_re
|
||||
|
||||
|
||||
spot, margin, futures = TradingMode.SPOT, TradingMode.MARGIN, TradingMode.FUTURES
|
||||
|
||||
|
||||
def test_init_create_session(default_conf):
|
||||
# Check if init create a session
|
||||
init_db(default_conf['db_url'])
|
||||
assert hasattr(Trade, '_session')
|
||||
assert 'scoped_session' in type(Trade._session).__name__
|
||||
|
||||
|
||||
def test_init_custom_db_url(default_conf, tmpdir):
|
||||
# Update path to a value other than default, but still in-memory
|
||||
filename = f"{tmpdir}/freqtrade2_test.sqlite"
|
||||
assert not Path(filename).is_file()
|
||||
|
||||
default_conf.update({'db_url': f'sqlite:///{filename}'})
|
||||
|
||||
init_db(default_conf['db_url'])
|
||||
assert Path(filename).is_file()
|
||||
r = Trade._session.execute(text("PRAGMA journal_mode"))
|
||||
assert r.first() == ('wal',)
|
||||
|
||||
|
||||
def test_init_invalid_db_url():
|
||||
# Update path to a value other than default, but still in-memory
|
||||
with pytest.raises(OperationalException, match=r'.*no valid database URL*'):
|
||||
init_db('unknown:///some.url')
|
||||
|
||||
with pytest.raises(OperationalException, match=r'Bad db-url.*For in-memory database, pl.*'):
|
||||
init_db('sqlite:///')
|
||||
|
||||
|
||||
def test_init_prod_db(default_conf, mocker):
|
||||
default_conf.update({'dry_run': False})
|
||||
default_conf.update({'db_url': DEFAULT_DB_PROD_URL})
|
||||
|
||||
create_engine_mock = mocker.patch('freqtrade.persistence.models.create_engine', MagicMock())
|
||||
|
||||
init_db(default_conf['db_url'])
|
||||
assert create_engine_mock.call_count == 1
|
||||
assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tradesv3.sqlite'
|
||||
|
||||
|
||||
def test_init_dryrun_db(default_conf, tmpdir):
|
||||
filename = f"{tmpdir}/freqtrade2_prod.sqlite"
|
||||
assert not Path(filename).is_file()
|
||||
default_conf.update({
|
||||
'dry_run': True,
|
||||
'db_url': f'sqlite:///{filename}'
|
||||
})
|
||||
|
||||
init_db(default_conf['db_url'])
|
||||
assert Path(filename).is_file()
|
||||
|
||||
|
||||
@pytest.mark.parametrize('is_short', [False, True])
|
||||
@pytest.mark.usefixtures("init_persistence")
|
||||
def test_enter_exit_side(fee, is_short):
|
||||
@@ -316,8 +259,7 @@ def test_interest(fee, exchange, is_short, lev, minutes, rate, interest,
|
||||
(True, 3.0, 30.0, margin),
|
||||
])
|
||||
@pytest.mark.usefixtures("init_persistence")
|
||||
def test_borrowed(limit_buy_order_usdt, limit_sell_order_usdt, fee,
|
||||
caplog, is_short, lev, borrowed, trading_mode):
|
||||
def test_borrowed(fee, is_short, lev, borrowed, trading_mode):
|
||||
"""
|
||||
10 minute limit trade on Binance/Kraken at 1x, 3x leverage
|
||||
fee: 0.25% quote
|
||||
@@ -1204,347 +1146,6 @@ def test_calc_profit(
|
||||
trade.open_rate)) == round(profit_ratio, 8)
|
||||
|
||||
|
||||
def test_migrate_new(mocker, default_conf, fee, caplog):
|
||||
"""
|
||||
Test Database migration (starting with new pairformat)
|
||||
"""
|
||||
caplog.set_level(logging.DEBUG)
|
||||
amount = 103.223
|
||||
# Always create all columns apart from the last!
|
||||
create_table_old = """CREATE TABLE IF NOT EXISTS "trades" (
|
||||
id INTEGER NOT NULL,
|
||||
exchange VARCHAR NOT NULL,
|
||||
pair VARCHAR NOT NULL,
|
||||
is_open BOOLEAN NOT NULL,
|
||||
fee FLOAT NOT NULL,
|
||||
open_rate FLOAT,
|
||||
close_rate FLOAT,
|
||||
close_profit FLOAT,
|
||||
stake_amount FLOAT NOT NULL,
|
||||
amount FLOAT,
|
||||
open_date DATETIME NOT NULL,
|
||||
close_date DATETIME,
|
||||
open_order_id VARCHAR,
|
||||
stop_loss FLOAT,
|
||||
initial_stop_loss FLOAT,
|
||||
max_rate FLOAT,
|
||||
sell_reason VARCHAR,
|
||||
strategy VARCHAR,
|
||||
ticker_interval INTEGER,
|
||||
stoploss_order_id VARCHAR,
|
||||
PRIMARY KEY (id),
|
||||
CHECK (is_open IN (0, 1))
|
||||
);"""
|
||||
create_table_order = """CREATE TABLE orders (
|
||||
id INTEGER NOT NULL,
|
||||
ft_trade_id INTEGER,
|
||||
ft_order_side VARCHAR(25) NOT NULL,
|
||||
ft_pair VARCHAR(25) NOT NULL,
|
||||
ft_is_open BOOLEAN NOT NULL,
|
||||
order_id VARCHAR(255) NOT NULL,
|
||||
status VARCHAR(255),
|
||||
symbol VARCHAR(25),
|
||||
order_type VARCHAR(50),
|
||||
side VARCHAR(25),
|
||||
price FLOAT,
|
||||
amount FLOAT,
|
||||
filled FLOAT,
|
||||
remaining FLOAT,
|
||||
cost FLOAT,
|
||||
order_date DATETIME,
|
||||
order_filled_date DATETIME,
|
||||
order_update_date DATETIME,
|
||||
PRIMARY KEY (id)
|
||||
);"""
|
||||
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, ticker_interval,
|
||||
open_order_id, stoploss_order_id)
|
||||
VALUES ('binance', 'ETC/BTC', 1, {fee},
|
||||
0.00258580, {stake}, {amount},
|
||||
'2019-11-28 12:44:24.000000',
|
||||
0.0, 0.0, 0.0, '5m',
|
||||
'buy_order', 'dry_stop_order_id222')
|
||||
""".format(fee=fee.return_value,
|
||||
stake=default_conf.get("stake_amount"),
|
||||
amount=amount
|
||||
)
|
||||
insert_orders = f"""
|
||||
insert into orders (
|
||||
ft_trade_id,
|
||||
ft_order_side,
|
||||
ft_pair,
|
||||
ft_is_open,
|
||||
order_id,
|
||||
status,
|
||||
symbol,
|
||||
order_type,
|
||||
side,
|
||||
price,
|
||||
amount,
|
||||
filled,
|
||||
remaining,
|
||||
cost)
|
||||
values (
|
||||
1,
|
||||
'buy',
|
||||
'ETC/BTC',
|
||||
0,
|
||||
'dry_buy_order',
|
||||
'closed',
|
||||
'ETC/BTC',
|
||||
'limit',
|
||||
'buy',
|
||||
0.00258580,
|
||||
{amount},
|
||||
{amount},
|
||||
0,
|
||||
{amount * 0.00258580}
|
||||
),
|
||||
(
|
||||
1,
|
||||
'buy',
|
||||
'ETC/BTC',
|
||||
1,
|
||||
'dry_buy_order22',
|
||||
'canceled',
|
||||
'ETC/BTC',
|
||||
'limit',
|
||||
'buy',
|
||||
0.00258580,
|
||||
{amount},
|
||||
{amount},
|
||||
0,
|
||||
{amount * 0.00258580}
|
||||
),
|
||||
(
|
||||
1,
|
||||
'stoploss',
|
||||
'ETC/BTC',
|
||||
1,
|
||||
'dry_stop_order_id11X',
|
||||
'canceled',
|
||||
'ETC/BTC',
|
||||
'limit',
|
||||
'sell',
|
||||
0.00258580,
|
||||
{amount},
|
||||
{amount},
|
||||
0,
|
||||
{amount * 0.00258580}
|
||||
),
|
||||
(
|
||||
1,
|
||||
'stoploss',
|
||||
'ETC/BTC',
|
||||
1,
|
||||
'dry_stop_order_id222',
|
||||
'open',
|
||||
'ETC/BTC',
|
||||
'limit',
|
||||
'sell',
|
||||
0.00258580,
|
||||
{amount},
|
||||
{amount},
|
||||
0,
|
||||
{amount * 0.00258580}
|
||||
)
|
||||
"""
|
||||
engine = create_engine('sqlite://')
|
||||
mocker.patch('freqtrade.persistence.models.create_engine', lambda *args, **kwargs: engine)
|
||||
|
||||
# Create table using the old format
|
||||
with engine.begin() as connection:
|
||||
connection.execute(text(create_table_old))
|
||||
connection.execute(text(create_table_order))
|
||||
connection.execute(text("create index ix_trades_is_open on trades(is_open)"))
|
||||
connection.execute(text("create index ix_trades_pair on trades(pair)"))
|
||||
connection.execute(text(insert_table_old))
|
||||
connection.execute(text(insert_orders))
|
||||
|
||||
# fake previous backup
|
||||
connection.execute(text("create table trades_bak as select * from trades"))
|
||||
|
||||
connection.execute(text("create table trades_bak1 as select * from trades"))
|
||||
# Run init to test migration
|
||||
init_db(default_conf['db_url'])
|
||||
|
||||
assert len(Trade.query.filter(Trade.id == 1).all()) == 1
|
||||
trade = Trade.query.filter(Trade.id == 1).first()
|
||||
assert trade.fee_open == fee.return_value
|
||||
assert trade.fee_close == fee.return_value
|
||||
assert trade.open_rate_requested is None
|
||||
assert trade.close_rate_requested is None
|
||||
assert trade.is_open == 1
|
||||
assert trade.amount == amount
|
||||
assert trade.amount_requested == amount
|
||||
assert trade.stake_amount == default_conf.get("stake_amount")
|
||||
assert trade.pair == "ETC/BTC"
|
||||
assert trade.exchange == "binance"
|
||||
assert trade.max_rate == 0.0
|
||||
assert trade.min_rate is None
|
||||
assert trade.stop_loss == 0.0
|
||||
assert trade.initial_stop_loss == 0.0
|
||||
assert trade.exit_reason is None
|
||||
assert trade.strategy is None
|
||||
assert trade.timeframe == '5m'
|
||||
assert trade.stoploss_order_id == 'dry_stop_order_id222'
|
||||
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 for trades - backup: trades_bak2, orders_bak0",
|
||||
caplog)
|
||||
assert log_has("Database migration finished.", caplog)
|
||||
assert pytest.approx(trade.open_trade_value) == trade._calc_open_trade_value(
|
||||
trade.amount, trade.open_rate)
|
||||
assert trade.close_profit_abs is None
|
||||
|
||||
orders = trade.orders
|
||||
assert len(orders) == 4
|
||||
assert orders[0].order_id == 'dry_buy_order'
|
||||
assert orders[0].ft_order_side == 'buy'
|
||||
|
||||
assert orders[-1].order_id == 'dry_stop_order_id222'
|
||||
assert orders[-1].ft_order_side == 'stoploss'
|
||||
assert orders[-1].ft_is_open is True
|
||||
|
||||
assert orders[1].order_id == 'dry_buy_order22'
|
||||
assert orders[1].ft_order_side == 'buy'
|
||||
assert orders[1].ft_is_open is False
|
||||
|
||||
assert orders[2].order_id == 'dry_stop_order_id11X'
|
||||
assert orders[2].ft_order_side == 'stoploss'
|
||||
assert orders[2].ft_is_open is False
|
||||
|
||||
|
||||
def test_migrate_too_old(mocker, default_conf, fee, caplog):
|
||||
"""
|
||||
Test Database migration (starting with new pairformat)
|
||||
"""
|
||||
caplog.set_level(logging.DEBUG)
|
||||
amount = 103.223
|
||||
create_table_old = """CREATE TABLE IF NOT EXISTS "trades" (
|
||||
id INTEGER NOT NULL,
|
||||
exchange VARCHAR NOT NULL,
|
||||
pair VARCHAR NOT NULL,
|
||||
is_open BOOLEAN NOT NULL,
|
||||
fee_open FLOAT NOT NULL,
|
||||
fee_close FLOAT NOT NULL,
|
||||
open_rate FLOAT,
|
||||
close_rate FLOAT,
|
||||
close_profit FLOAT,
|
||||
stake_amount FLOAT NOT NULL,
|
||||
amount FLOAT,
|
||||
open_date DATETIME NOT NULL,
|
||||
close_date DATETIME,
|
||||
open_order_id VARCHAR,
|
||||
PRIMARY KEY (id),
|
||||
CHECK (is_open IN (0, 1))
|
||||
);"""
|
||||
|
||||
insert_table_old = """INSERT INTO trades (exchange, pair, is_open, fee_open, fee_close,
|
||||
open_rate, stake_amount, amount, open_date)
|
||||
VALUES ('binance', 'ETC/BTC', 1, {fee}, {fee},
|
||||
0.00258580, {stake}, {amount},
|
||||
'2019-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.models.create_engine', lambda *args, **kwargs: engine)
|
||||
|
||||
# Create table using the old format
|
||||
with engine.begin() as connection:
|
||||
connection.execute(text(create_table_old))
|
||||
connection.execute(text(insert_table_old))
|
||||
|
||||
# Run init to test migration
|
||||
with pytest.raises(OperationalException, match=r'Your database seems to be very old'):
|
||||
init_db(default_conf['db_url'])
|
||||
|
||||
|
||||
def test_migrate_get_last_sequence_ids():
|
||||
engine = MagicMock()
|
||||
engine.begin = MagicMock()
|
||||
engine.name = 'postgresql'
|
||||
get_last_sequence_ids(engine, 'trades_bak', 'orders_bak')
|
||||
|
||||
assert engine.begin.call_count == 2
|
||||
engine.reset_mock()
|
||||
engine.begin.reset_mock()
|
||||
|
||||
engine.name = 'somethingelse'
|
||||
get_last_sequence_ids(engine, 'trades_bak', 'orders_bak')
|
||||
|
||||
assert engine.begin.call_count == 0
|
||||
|
||||
|
||||
def test_migrate_set_sequence_ids():
|
||||
engine = MagicMock()
|
||||
engine.begin = MagicMock()
|
||||
engine.name = 'postgresql'
|
||||
set_sequence_ids(engine, 22, 55, 5)
|
||||
|
||||
assert engine.begin.call_count == 1
|
||||
engine.reset_mock()
|
||||
engine.begin.reset_mock()
|
||||
|
||||
engine.name = 'somethingelse'
|
||||
set_sequence_ids(engine, 22, 55, 6)
|
||||
|
||||
assert engine.begin.call_count == 0
|
||||
|
||||
|
||||
def test_migrate_pairlocks(mocker, default_conf, fee, caplog):
|
||||
"""
|
||||
Test Database migration (starting with new pairformat)
|
||||
"""
|
||||
caplog.set_level(logging.DEBUG)
|
||||
# Always create all columns apart from the last!
|
||||
create_table_old = """CREATE TABLE pairlocks (
|
||||
id INTEGER NOT NULL,
|
||||
pair VARCHAR(25) NOT NULL,
|
||||
reason VARCHAR(255),
|
||||
lock_time DATETIME NOT NULL,
|
||||
lock_end_time DATETIME NOT NULL,
|
||||
active BOOLEAN NOT NULL,
|
||||
PRIMARY KEY (id)
|
||||
)
|
||||
"""
|
||||
create_index1 = "CREATE INDEX ix_pairlocks_pair ON pairlocks (pair)"
|
||||
create_index2 = "CREATE INDEX ix_pairlocks_lock_end_time ON pairlocks (lock_end_time)"
|
||||
create_index3 = "CREATE INDEX ix_pairlocks_active ON pairlocks (active)"
|
||||
insert_table_old = """INSERT INTO pairlocks (
|
||||
id, pair, reason, lock_time, lock_end_time, active)
|
||||
VALUES (1, 'ETH/BTC', 'Auto lock', '2021-07-12 18:41:03', '2021-07-11 18:45:00', 1)
|
||||
"""
|
||||
insert_table_old2 = """INSERT INTO pairlocks (
|
||||
id, pair, reason, lock_time, lock_end_time, active)
|
||||
VALUES (2, '*', 'Lock all', '2021-07-12 18:41:03', '2021-07-12 19:00:00', 1)
|
||||
"""
|
||||
engine = create_engine('sqlite://')
|
||||
mocker.patch('freqtrade.persistence.models.create_engine', lambda *args, **kwargs: engine)
|
||||
# Create table using the old format
|
||||
with engine.begin() as connection:
|
||||
connection.execute(text(create_table_old))
|
||||
|
||||
connection.execute(text(insert_table_old))
|
||||
connection.execute(text(insert_table_old2))
|
||||
connection.execute(text(create_index1))
|
||||
connection.execute(text(create_index2))
|
||||
connection.execute(text(create_index3))
|
||||
|
||||
init_db(default_conf['db_url'])
|
||||
|
||||
assert len(PairLock.query.all()) == 2
|
||||
assert len(PairLock.query.filter(PairLock.pair == '*').all()) == 1
|
||||
pairlocks = PairLock.query.filter(PairLock.pair == 'ETH/BTC').all()
|
||||
assert len(pairlocks) == 1
|
||||
pairlocks[0].pair == 'ETH/BTC'
|
||||
pairlocks[0].side == '*'
|
||||
|
||||
|
||||
def test_adjust_stop_loss(fee):
|
||||
trade = Trade(
|
||||
pair='ADA/USDT',
|
||||
@@ -1758,16 +1359,17 @@ def test_to_json(fee):
|
||||
'amount': 123.0,
|
||||
'amount_requested': 123.0,
|
||||
'stake_amount': 0.001,
|
||||
'max_stake_amount': None,
|
||||
'trade_duration': None,
|
||||
'trade_duration_s': None,
|
||||
'realized_profit': 0.0,
|
||||
'realized_profit_ratio': None,
|
||||
'close_profit': None,
|
||||
'close_profit_pct': None,
|
||||
'close_profit_abs': None,
|
||||
'profit_ratio': None,
|
||||
'profit_pct': None,
|
||||
'profit_abs': None,
|
||||
'sell_reason': None,
|
||||
'exit_reason': None,
|
||||
'exit_order_status': None,
|
||||
'stop_loss_abs': None,
|
||||
@@ -1782,7 +1384,6 @@ def test_to_json(fee):
|
||||
'min_rate': None,
|
||||
'max_rate': None,
|
||||
'strategy': None,
|
||||
'buy_tag': None,
|
||||
'enter_tag': None,
|
||||
'timeframe': None,
|
||||
'exchange': 'binance',
|
||||
@@ -1826,6 +1427,7 @@ def test_to_json(fee):
|
||||
'amount': 100.0,
|
||||
'amount_requested': 101.0,
|
||||
'stake_amount': 0.001,
|
||||
'max_stake_amount': None,
|
||||
'trade_duration': 60,
|
||||
'trade_duration_s': 3600,
|
||||
'stop_loss_abs': None,
|
||||
@@ -1838,6 +1440,7 @@ def test_to_json(fee):
|
||||
'initial_stop_loss_pct': None,
|
||||
'initial_stop_loss_ratio': None,
|
||||
'realized_profit': 0.0,
|
||||
'realized_profit_ratio': None,
|
||||
'close_profit': None,
|
||||
'close_profit_pct': None,
|
||||
'close_profit_abs': None,
|
||||
@@ -1857,11 +1460,9 @@ def test_to_json(fee):
|
||||
'open_order_id': None,
|
||||
'open_rate_requested': None,
|
||||
'open_trade_value': 12.33075,
|
||||
'sell_reason': None,
|
||||
'exit_reason': None,
|
||||
'exit_order_status': None,
|
||||
'strategy': None,
|
||||
'buy_tag': 'buys_signal_001',
|
||||
'enter_tag': 'buys_signal_001',
|
||||
'timeframe': None,
|
||||
'exchange': 'binance',
|
||||
@@ -1894,7 +1495,7 @@ def test_stoploss_reinitialization(default_conf, fee):
|
||||
assert trade.stop_loss_pct == -0.05
|
||||
assert trade.initial_stop_loss == 0.95
|
||||
assert trade.initial_stop_loss_pct == -0.05
|
||||
Trade.query.session.add(trade)
|
||||
Trade.session.add(trade)
|
||||
Trade.commit()
|
||||
|
||||
# Lower stoploss
|
||||
@@ -1956,7 +1557,7 @@ def test_stoploss_reinitialization_leverage(default_conf, fee):
|
||||
assert trade.stop_loss_pct == -0.1
|
||||
assert trade.initial_stop_loss == 0.98
|
||||
assert trade.initial_stop_loss_pct == -0.1
|
||||
Trade.query.session.add(trade)
|
||||
Trade.session.add(trade)
|
||||
Trade.commit()
|
||||
|
||||
# Lower stoploss
|
||||
@@ -2018,7 +1619,7 @@ def test_stoploss_reinitialization_short(default_conf, fee):
|
||||
assert trade.stop_loss_pct == -0.1
|
||||
assert trade.initial_stop_loss == 1.02
|
||||
assert trade.initial_stop_loss_pct == -0.1
|
||||
Trade.query.session.add(trade)
|
||||
Trade.session.add(trade)
|
||||
Trade.commit()
|
||||
# Lower stoploss
|
||||
Trade.stoploss_reinitialization(-0.15)
|
||||
@@ -2193,17 +1794,17 @@ def test_get_trades_proxy(fee, use_db, is_short):
|
||||
@pytest.mark.usefixtures("init_persistence")
|
||||
@pytest.mark.parametrize('is_short', [True, False])
|
||||
def test_get_trades__query(fee, is_short):
|
||||
query = Trade.get_trades([])
|
||||
query = Trade.get_trades_query([])
|
||||
# without orders there should be no join issued.
|
||||
query1 = Trade.get_trades([], include_orders=False)
|
||||
query1 = Trade.get_trades_query([], include_orders=False)
|
||||
|
||||
# Empty "with-options -> default - selectin"
|
||||
assert query._with_options == ()
|
||||
assert query1._with_options != ()
|
||||
|
||||
create_mock_trades(fee, is_short)
|
||||
query = Trade.get_trades([])
|
||||
query1 = Trade.get_trades([], include_orders=False)
|
||||
query = Trade.get_trades_query([])
|
||||
query1 = Trade.get_trades_query([], include_orders=False)
|
||||
|
||||
assert query._with_options == ()
|
||||
assert query1._with_options != ()
|
||||
@@ -2270,13 +1871,18 @@ def test_get_exit_order_count(fee, is_short):
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_persistence")
|
||||
def test_update_order_from_ccxt(caplog):
|
||||
def test_update_order_from_ccxt(caplog, time_machine):
|
||||
start = datetime(2023, 1, 1, 4, tzinfo=timezone.utc)
|
||||
time_machine.move_to(start, tick=False)
|
||||
|
||||
# Most basic order return (only has orderid)
|
||||
o = Order.parse_from_ccxt_object({'id': '1234'}, 'ADA/USDT', 'buy')
|
||||
o = Order.parse_from_ccxt_object({'id': '1234'}, 'ADA/USDT', 'buy', 20.01, 1234.6)
|
||||
assert isinstance(o, Order)
|
||||
assert o.ft_pair == 'ADA/USDT'
|
||||
assert o.ft_order_side == 'buy'
|
||||
assert o.order_id == '1234'
|
||||
assert o.ft_price == 1234.6
|
||||
assert o.ft_amount == 20.01
|
||||
assert o.ft_is_open
|
||||
ccxt_order = {
|
||||
'id': '1234',
|
||||
@@ -2290,13 +1896,15 @@ def test_update_order_from_ccxt(caplog):
|
||||
'status': 'open',
|
||||
'timestamp': 1599394315123
|
||||
}
|
||||
o = Order.parse_from_ccxt_object(ccxt_order, 'ADA/USDT', 'buy')
|
||||
o = Order.parse_from_ccxt_object(ccxt_order, 'ADA/USDT', 'buy', 20.01, 1234.6)
|
||||
assert isinstance(o, Order)
|
||||
assert o.ft_pair == 'ADA/USDT'
|
||||
assert o.ft_order_side == 'buy'
|
||||
assert o.order_id == '1234'
|
||||
assert o.order_type == 'limit'
|
||||
assert o.price == 1234.5
|
||||
assert o.ft_price == 1234.6
|
||||
assert o.ft_amount == 20.01
|
||||
assert o.filled == 9
|
||||
assert o.remaining == 11
|
||||
assert o.order_date is not None
|
||||
@@ -2315,7 +1923,9 @@ def test_update_order_from_ccxt(caplog):
|
||||
assert o.filled == 20.0
|
||||
assert o.remaining == 0.0
|
||||
assert not o.ft_is_open
|
||||
assert o.order_filled_date is not None
|
||||
assert o.order_filled_date == start
|
||||
# Move time
|
||||
time_machine.move_to(start + timedelta(hours=1), tick=False)
|
||||
|
||||
ccxt_order.update({'id': 'somethingelse'})
|
||||
with pytest.raises(DependencyException, match=r"Order-id's don't match"):
|
||||
@@ -2328,6 +1938,12 @@ def test_update_order_from_ccxt(caplog):
|
||||
|
||||
# Call regular update - shouldn't fail.
|
||||
Order.update_orders([o], {'id': '1234'})
|
||||
assert o.order_filled_date == start
|
||||
|
||||
# Fill order again - shouldn't update filled date
|
||||
ccxt_order.update({'id': '1234'})
|
||||
Order.update_orders([o], ccxt_order)
|
||||
assert o.order_filled_date == start
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_persistence")
|
||||
@@ -2401,6 +2017,7 @@ def test_Trade_object_idem():
|
||||
'get_open_trades_without_assigned_fees',
|
||||
'get_open_order_trades',
|
||||
'get_trades',
|
||||
'get_trades_query',
|
||||
'get_exit_reason_performance',
|
||||
'get_enter_tag_performance',
|
||||
'get_mix_tag_performance',
|
||||
@@ -2827,8 +2444,9 @@ def test_select_filled_orders(fee):
|
||||
def test_order_to_ccxt(limit_buy_order_open):
|
||||
|
||||
order = Order.parse_from_ccxt_object(limit_buy_order_open, 'mocked', 'buy')
|
||||
order.query.session.add(order)
|
||||
Order.query.session.commit()
|
||||
order.ft_trade_id = 1
|
||||
order.session.add(order)
|
||||
Order.session.commit()
|
||||
|
||||
order_resp = Order.order_by_id(limit_buy_order_open['id'])
|
||||
assert order_resp
|
||||
@@ -2930,7 +2548,7 @@ def test_recalc_trade_from_orders_dca(data) -> None:
|
||||
leverage=1.0,
|
||||
trading_mode=TradingMode.SPOT
|
||||
)
|
||||
Trade.query.session.add(trade)
|
||||
Trade.session.add(trade)
|
||||
|
||||
for idx, (order, result) in enumerate(data['orders']):
|
||||
amount = order[1]
|
||||
@@ -2941,6 +2559,8 @@ def test_recalc_trade_from_orders_dca(data) -> None:
|
||||
ft_pair=trade.pair,
|
||||
order_id=f"order_{order[0]}_{idx}",
|
||||
ft_is_open=False,
|
||||
ft_amount=amount,
|
||||
ft_price=price,
|
||||
status="closed",
|
||||
symbol=trade.pair,
|
||||
order_type="market",
|
||||
@@ -2957,11 +2577,11 @@ def test_recalc_trade_from_orders_dca(data) -> None:
|
||||
trade.recalc_trade_from_orders()
|
||||
Trade.commit()
|
||||
|
||||
orders1 = Order.query.all()
|
||||
orders1 = Order.session.scalars(select(Order)).all()
|
||||
assert orders1
|
||||
assert len(orders1) == idx + 1
|
||||
|
||||
trade = Trade.query.first()
|
||||
trade = Trade.session.scalars(select(Trade)).first()
|
||||
assert trade
|
||||
assert len(trade.orders) == idx + 1
|
||||
if idx < len(data) - 1:
|
||||
@@ -2978,6 +2598,6 @@ def test_recalc_trade_from_orders_dca(data) -> None:
|
||||
assert pytest.approx(trade.close_profit_abs) == data['end_profit']
|
||||
assert pytest.approx(trade.close_profit) == data['end_profit_ratio']
|
||||
assert not trade.is_open
|
||||
trade = Trade.query.first()
|
||||
trade = Trade.session.scalars(select(Trade)).first()
|
||||
assert trade
|
||||
assert trade.open_order_id is None
|
||||
|
@@ -18,8 +18,13 @@ from freqtrade.persistence import Trade
|
||||
from freqtrade.plugins.pairlist.pairlist_helpers import dynamic_expand_pairlist, expand_pairlist
|
||||
from freqtrade.plugins.pairlistmanager import PairListManager
|
||||
from freqtrade.resolvers import PairListResolver
|
||||
from tests.conftest import (create_mock_trades_usdt, get_patched_exchange, get_patched_freqtradebot,
|
||||
log_has, log_has_re, num_log_has)
|
||||
from tests.conftest import (EXMS, create_mock_trades_usdt, get_patched_exchange,
|
||||
get_patched_freqtradebot, log_has, log_has_re, num_log_has)
|
||||
|
||||
|
||||
# Exclude RemotePairList from tests.
|
||||
# It has a mandatory parameter, and requires special handling, which happens in test_remotepairlist.
|
||||
TESTABLE_PAIRLISTS = [p for p in AVAILABLE_PAIRLISTS if p not in ['RemotePairList']]
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
@@ -111,7 +116,7 @@ def static_pl_conf(whitelist_conf):
|
||||
|
||||
|
||||
def test_log_cached(mocker, static_pl_conf, markets, tickers):
|
||||
mocker.patch.multiple('freqtrade.exchange.Exchange',
|
||||
mocker.patch.multiple(EXMS,
|
||||
markets=PropertyMock(return_value=markets),
|
||||
exchange_has=MagicMock(return_value=True),
|
||||
get_tickers=tickers
|
||||
@@ -134,7 +139,7 @@ def test_log_cached(mocker, static_pl_conf, markets, tickers):
|
||||
|
||||
def test_load_pairlist_noexist(mocker, markets, default_conf):
|
||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||
mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets))
|
||||
mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets))
|
||||
plm = PairListManager(freqtrade.exchange, default_conf, MagicMock())
|
||||
with pytest.raises(OperationalException,
|
||||
match=r"Impossible to load Pairlist 'NonexistingPairList'. "
|
||||
@@ -145,7 +150,7 @@ def test_load_pairlist_noexist(mocker, markets, default_conf):
|
||||
|
||||
def test_load_pairlist_verify_multi(mocker, markets_static, default_conf):
|
||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||
mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets_static))
|
||||
mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets_static))
|
||||
plm = PairListManager(freqtrade.exchange, default_conf, MagicMock())
|
||||
# Call different versions one after the other, should always consider what was passed in
|
||||
# and have no side-effects (therefore the same check multiple times)
|
||||
@@ -161,7 +166,7 @@ def test_refresh_market_pair_not_in_whitelist(mocker, markets, static_pl_conf):
|
||||
|
||||
freqtrade = get_patched_freqtradebot(mocker, static_pl_conf)
|
||||
|
||||
mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets))
|
||||
mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets))
|
||||
freqtrade.pairlists.refresh_pairlist()
|
||||
# List ordered by BaseVolume
|
||||
whitelist = ['ETH/BTC', 'TKN/BTC']
|
||||
@@ -175,7 +180,7 @@ def test_refresh_market_pair_not_in_whitelist(mocker, markets, static_pl_conf):
|
||||
def test_refresh_static_pairlist(mocker, markets, static_pl_conf):
|
||||
freqtrade = get_patched_freqtradebot(mocker, static_pl_conf)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
exchange_has=MagicMock(return_value=True),
|
||||
markets=PropertyMock(return_value=markets),
|
||||
)
|
||||
@@ -199,7 +204,7 @@ def test_refresh_static_pairlist_noexist(mocker, markets, static_pl_conf, pairs,
|
||||
static_pl_conf['exchange']['pair_whitelist'] += pairs
|
||||
freqtrade = get_patched_freqtradebot(mocker, static_pl_conf)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
exchange_has=MagicMock(return_value=True),
|
||||
markets=PropertyMock(return_value=markets),
|
||||
)
|
||||
@@ -216,7 +221,7 @@ def test_invalid_blacklist(mocker, markets, static_pl_conf, caplog):
|
||||
static_pl_conf['exchange']['pair_blacklist'] = ['*/BTC']
|
||||
freqtrade = get_patched_freqtradebot(mocker, static_pl_conf)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
exchange_has=MagicMock(return_value=True),
|
||||
markets=PropertyMock(return_value=markets),
|
||||
)
|
||||
@@ -232,7 +237,7 @@ def test_remove_logs_for_pairs_already_in_blacklist(mocker, markets, static_pl_c
|
||||
logger = logging.getLogger(__name__)
|
||||
freqtrade = get_patched_freqtradebot(mocker, static_pl_conf)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
exchange_has=MagicMock(return_value=True),
|
||||
markets=PropertyMock(return_value=markets),
|
||||
)
|
||||
@@ -259,14 +264,14 @@ def test_remove_logs_for_pairs_already_in_blacklist(mocker, markets, static_pl_c
|
||||
def test_refresh_pairlist_dynamic(mocker, shitcoinmarkets, tickers, whitelist_conf):
|
||||
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
get_tickers=tickers,
|
||||
exchange_has=MagicMock(return_value=True),
|
||||
)
|
||||
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',
|
||||
EXMS,
|
||||
markets=PropertyMock(return_value=shitcoinmarkets),
|
||||
)
|
||||
# argument: use the whitelist dynamically by exchange-volume
|
||||
@@ -286,7 +291,7 @@ def test_refresh_pairlist_dynamic_2(mocker, shitcoinmarkets, tickers, whitelist_
|
||||
tickers_dict = tickers()
|
||||
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
exchange_has=MagicMock(return_value=True),
|
||||
)
|
||||
# Remove caching of ticker data to emulate changing volume by the time of second call
|
||||
@@ -297,7 +302,7 @@ def test_refresh_pairlist_dynamic_2(mocker, shitcoinmarkets, tickers, whitelist_
|
||||
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',
|
||||
EXMS,
|
||||
markets=PropertyMock(return_value=shitcoinmarkets),
|
||||
)
|
||||
|
||||
@@ -315,11 +320,11 @@ def test_refresh_pairlist_dynamic_2(mocker, shitcoinmarkets, tickers, whitelist_
|
||||
|
||||
def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf):
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
exchange_has=MagicMock(return_value=True),
|
||||
)
|
||||
freqtrade = get_patched_freqtradebot(mocker, whitelist_conf)
|
||||
mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets_empty))
|
||||
mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets_empty))
|
||||
|
||||
# argument: use the whitelist dynamically by exchange-volume
|
||||
whitelist = []
|
||||
@@ -518,15 +523,15 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t
|
||||
('HOT/BTC', '1d', CandleType.SPOT): ohlcv_history_high_vola,
|
||||
}
|
||||
|
||||
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
|
||||
mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=True))
|
||||
|
||||
freqtrade = get_patched_freqtradebot(mocker, whitelist_conf)
|
||||
mocker.patch.multiple('freqtrade.exchange.Exchange',
|
||||
mocker.patch.multiple(EXMS,
|
||||
get_tickers=tickers,
|
||||
markets=PropertyMock(return_value=shitcoinmarkets)
|
||||
)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
refresh_latest_ohlcv=MagicMock(return_value=ohlcv_data),
|
||||
)
|
||||
|
||||
@@ -644,7 +649,7 @@ def test_VolumePairList_range(mocker, whitelist_conf, shitcoinmarkets, tickers,
|
||||
('HOT/BTC', '1d', CandleType.SPOT): ohlcv_history_high_volume,
|
||||
}
|
||||
|
||||
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
|
||||
mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=True))
|
||||
|
||||
if volumefilter_result == 'default_refresh_too_short':
|
||||
with pytest.raises(OperationalException,
|
||||
@@ -670,7 +675,7 @@ def test_VolumePairList_range(mocker, whitelist_conf, shitcoinmarkets, tickers,
|
||||
else:
|
||||
freqtrade = get_patched_freqtradebot(mocker, whitelist_conf)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
get_tickers=tickers,
|
||||
markets=PropertyMock(return_value=shitcoinmarkets)
|
||||
)
|
||||
@@ -682,7 +687,7 @@ def test_VolumePairList_range(mocker, whitelist_conf, shitcoinmarkets, tickers,
|
||||
ohlcv_data = []
|
||||
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
refresh_latest_ohlcv=MagicMock(return_value=ohlcv_data),
|
||||
)
|
||||
|
||||
@@ -697,7 +702,7 @@ def test_PrecisionFilter_error(mocker, whitelist_conf) -> None:
|
||||
whitelist_conf['pairlists'] = [{"method": "StaticPairList"}, {"method": "PrecisionFilter"}]
|
||||
del whitelist_conf['stoploss']
|
||||
|
||||
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
|
||||
mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=True))
|
||||
|
||||
with pytest.raises(OperationalException,
|
||||
match=r"PrecisionFilter can only work with stoploss defined\..*"):
|
||||
@@ -706,9 +711,9 @@ def test_PrecisionFilter_error(mocker, whitelist_conf) -> None:
|
||||
|
||||
def test_PerformanceFilter_error(mocker, whitelist_conf, caplog) -> None:
|
||||
whitelist_conf['pairlists'] = [{"method": "StaticPairList"}, {"method": "PerformanceFilter"}]
|
||||
if hasattr(Trade, 'query'):
|
||||
del Trade.query
|
||||
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
|
||||
if hasattr(Trade, 'session'):
|
||||
del Trade.session
|
||||
mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=True))
|
||||
exchange = get_patched_exchange(mocker, whitelist_conf)
|
||||
pm = PairListManager(exchange, whitelist_conf, MagicMock())
|
||||
pm.refresh_pairlist()
|
||||
@@ -750,7 +755,7 @@ def test_PerformanceFilter_lookback(mocker, default_conf_usdt, fee, caplog) -> N
|
||||
{"method": "StaticPairList"},
|
||||
{"method": "PerformanceFilter", "minutes": 60, "min_profit": 0.01}
|
||||
]
|
||||
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
|
||||
mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=True))
|
||||
exchange = get_patched_exchange(mocker, default_conf_usdt)
|
||||
pm = PairListManager(exchange, default_conf_usdt)
|
||||
pm.refresh_pairlist()
|
||||
@@ -776,7 +781,7 @@ def test_PerformanceFilter_keep_mid_order(mocker, default_conf_usdt, fee, caplog
|
||||
{"method": "StaticPairList", "allow_inactive": True},
|
||||
{"method": "PerformanceFilter", "minutes": 60, }
|
||||
]
|
||||
mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True)
|
||||
mocker.patch(f'{EXMS}.exchange_has', return_value=True)
|
||||
exchange = get_patched_exchange(mocker, default_conf_usdt)
|
||||
pm = PairListManager(exchange, default_conf_usdt)
|
||||
pm.refresh_pairlist()
|
||||
@@ -801,7 +806,7 @@ def test_PerformanceFilter_keep_mid_order(mocker, default_conf_usdt, fee, caplog
|
||||
def test_gen_pair_whitelist_not_supported(mocker, default_conf, tickers) -> None:
|
||||
default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}]
|
||||
|
||||
mocker.patch.multiple('freqtrade.exchange.Exchange',
|
||||
mocker.patch.multiple(EXMS,
|
||||
get_tickers=tickers,
|
||||
exchange_has=MagicMock(return_value=False),
|
||||
)
|
||||
@@ -814,7 +819,7 @@ def test_gen_pair_whitelist_not_supported(mocker, default_conf, tickers) -> None
|
||||
def test_pair_whitelist_not_supported_Spread(mocker, default_conf, tickers) -> None:
|
||||
default_conf['pairlists'] = [{'method': 'StaticPairList'}, {'method': 'SpreadFilter'}]
|
||||
|
||||
mocker.patch.multiple('freqtrade.exchange.Exchange',
|
||||
mocker.patch.multiple(EXMS,
|
||||
get_tickers=tickers,
|
||||
exchange_has=MagicMock(return_value=False),
|
||||
)
|
||||
@@ -823,11 +828,17 @@ def test_pair_whitelist_not_supported_Spread(mocker, default_conf, tickers) -> N
|
||||
match=r'Exchange does not support fetchTickers, .*'):
|
||||
get_patched_freqtradebot(mocker, default_conf)
|
||||
|
||||
mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=True))
|
||||
mocker.patch(f'{EXMS}.get_option', MagicMock(return_value=False))
|
||||
with pytest.raises(OperationalException,
|
||||
match=r'.*requires exchange to have bid/ask data'):
|
||||
get_patched_freqtradebot(mocker, default_conf)
|
||||
|
||||
@pytest.mark.parametrize("pairlist", AVAILABLE_PAIRLISTS)
|
||||
|
||||
@pytest.mark.parametrize("pairlist", TESTABLE_PAIRLISTS)
|
||||
def test_pairlist_class(mocker, whitelist_conf, markets, pairlist):
|
||||
whitelist_conf['pairlists'][0]['method'] = pairlist
|
||||
mocker.patch.multiple('freqtrade.exchange.Exchange',
|
||||
mocker.patch.multiple(EXMS,
|
||||
markets=PropertyMock(return_value=markets),
|
||||
exchange_has=MagicMock(return_value=True)
|
||||
)
|
||||
@@ -839,7 +850,7 @@ def test_pairlist_class(mocker, whitelist_conf, markets, pairlist):
|
||||
assert isinstance(freqtrade.pairlists.blacklist, list)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("pairlist", AVAILABLE_PAIRLISTS)
|
||||
@pytest.mark.parametrize("pairlist", TESTABLE_PAIRLISTS)
|
||||
@pytest.mark.parametrize("whitelist,log_message", [
|
||||
(['ETH/BTC', 'TKN/BTC'], ""),
|
||||
# TRX/ETH not in markets
|
||||
@@ -856,7 +867,7 @@ def test_pairlist_class(mocker, whitelist_conf, markets, pairlist):
|
||||
def test__whitelist_for_active_markets(mocker, whitelist_conf, markets, pairlist, whitelist, caplog,
|
||||
log_message, tickers):
|
||||
whitelist_conf['pairlists'][0]['method'] = pairlist
|
||||
mocker.patch.multiple('freqtrade.exchange.Exchange',
|
||||
mocker.patch.multiple(EXMS,
|
||||
markets=PropertyMock(return_value=markets),
|
||||
exchange_has=MagicMock(return_value=True),
|
||||
get_tickers=tickers
|
||||
@@ -872,14 +883,14 @@ def test__whitelist_for_active_markets(mocker, whitelist_conf, markets, pairlist
|
||||
assert log_message in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize("pairlist", AVAILABLE_PAIRLISTS)
|
||||
@pytest.mark.parametrize("pairlist", TESTABLE_PAIRLISTS)
|
||||
def test__whitelist_for_active_markets_empty(mocker, whitelist_conf, pairlist, tickers):
|
||||
whitelist_conf['pairlists'][0]['method'] = pairlist
|
||||
|
||||
mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True)
|
||||
mocker.patch(f'{EXMS}.exchange_has', return_value=True)
|
||||
|
||||
freqtrade = get_patched_freqtradebot(mocker, whitelist_conf)
|
||||
mocker.patch.multiple('freqtrade.exchange.Exchange',
|
||||
mocker.patch.multiple(EXMS,
|
||||
markets=PropertyMock(return_value=None),
|
||||
get_tickers=tickers
|
||||
)
|
||||
@@ -892,7 +903,7 @@ def test__whitelist_for_active_markets_empty(mocker, whitelist_conf, pairlist, t
|
||||
def test_volumepairlist_invalid_sortvalue(mocker, whitelist_conf):
|
||||
whitelist_conf['pairlists'][0].update({"sort_key": "asdf"})
|
||||
|
||||
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
|
||||
mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=True))
|
||||
with pytest.raises(OperationalException,
|
||||
match=r"key asdf not in .*"):
|
||||
get_patched_freqtradebot(mocker, whitelist_conf)
|
||||
@@ -900,7 +911,7 @@ def test_volumepairlist_invalid_sortvalue(mocker, whitelist_conf):
|
||||
|
||||
def test_volumepairlist_caching(mocker, markets, whitelist_conf, tickers):
|
||||
|
||||
mocker.patch.multiple('freqtrade.exchange.Exchange',
|
||||
mocker.patch.multiple(EXMS,
|
||||
markets=PropertyMock(return_value=markets),
|
||||
exchange_has=MagicMock(return_value=True),
|
||||
get_tickers=tickers
|
||||
@@ -920,7 +931,7 @@ def test_agefilter_min_days_listed_too_small(mocker, default_conf, markets, tick
|
||||
default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10},
|
||||
{'method': 'AgeFilter', 'min_days_listed': -1}]
|
||||
|
||||
mocker.patch.multiple('freqtrade.exchange.Exchange',
|
||||
mocker.patch.multiple(EXMS,
|
||||
markets=PropertyMock(return_value=markets),
|
||||
exchange_has=MagicMock(return_value=True),
|
||||
get_tickers=tickers
|
||||
@@ -936,7 +947,7 @@ def test_agefilter_max_days_lower_than_min_days(mocker, default_conf, markets, t
|
||||
{'method': 'AgeFilter', 'min_days_listed': 3,
|
||||
"max_days_listed": 2}]
|
||||
|
||||
mocker.patch.multiple('freqtrade.exchange.Exchange',
|
||||
mocker.patch.multiple(EXMS,
|
||||
markets=PropertyMock(return_value=markets),
|
||||
exchange_has=MagicMock(return_value=True),
|
||||
get_tickers=tickers
|
||||
@@ -951,7 +962,7 @@ def test_agefilter_min_days_listed_too_large(mocker, default_conf, markets, tick
|
||||
default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10},
|
||||
{'method': 'AgeFilter', 'min_days_listed': 99999}]
|
||||
|
||||
mocker.patch.multiple('freqtrade.exchange.Exchange',
|
||||
mocker.patch.multiple(EXMS,
|
||||
markets=PropertyMock(return_value=markets),
|
||||
exchange_has=MagicMock(return_value=True),
|
||||
get_tickers=tickers
|
||||
@@ -971,7 +982,7 @@ def test_agefilter_caching(mocker, markets, whitelist_conf_agefilter, tickers, o
|
||||
('LTC/BTC', '1d', CandleType.SPOT): ohlcv_history,
|
||||
}
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
markets=PropertyMock(return_value=markets),
|
||||
exchange_has=MagicMock(return_value=True),
|
||||
get_tickers=tickers,
|
||||
@@ -995,14 +1006,14 @@ def test_agefilter_caching(mocker, markets, whitelist_conf_agefilter, tickers, o
|
||||
('LTC/BTC', '1d', CandleType.SPOT): ohlcv_history,
|
||||
('XRP/BTC', '1d', CandleType.SPOT): ohlcv_history.iloc[[0]],
|
||||
}
|
||||
mocker.patch('freqtrade.exchange.Exchange.refresh_latest_ohlcv', return_value=ohlcv_data)
|
||||
mocker.patch(f'{EXMS}.refresh_latest_ohlcv', return_value=ohlcv_data)
|
||||
freqtrade.pairlists.refresh_pairlist()
|
||||
assert len(freqtrade.pairlists.whitelist) == 3
|
||||
assert freqtrade.exchange.refresh_latest_ohlcv.call_count == 1
|
||||
|
||||
# Move to next day
|
||||
t.move_to("2021-09-02 01:00:00 +00:00")
|
||||
mocker.patch('freqtrade.exchange.Exchange.refresh_latest_ohlcv', return_value=ohlcv_data)
|
||||
mocker.patch(f'{EXMS}.refresh_latest_ohlcv', return_value=ohlcv_data)
|
||||
freqtrade.pairlists.refresh_pairlist()
|
||||
assert len(freqtrade.pairlists.whitelist) == 3
|
||||
assert freqtrade.exchange.refresh_latest_ohlcv.call_count == 1
|
||||
@@ -1016,7 +1027,7 @@ def test_agefilter_caching(mocker, markets, whitelist_conf_agefilter, tickers, o
|
||||
('LTC/BTC', '1d', CandleType.SPOT): ohlcv_history,
|
||||
('XRP/BTC', '1d', CandleType.SPOT): ohlcv_history,
|
||||
}
|
||||
mocker.patch('freqtrade.exchange.Exchange.refresh_latest_ohlcv', return_value=ohlcv_data)
|
||||
mocker.patch(f'{EXMS}.refresh_latest_ohlcv', return_value=ohlcv_data)
|
||||
freqtrade.pairlists.refresh_pairlist()
|
||||
assert len(freqtrade.pairlists.whitelist) == 4
|
||||
# Called once (only for XRP/BTC)
|
||||
@@ -1028,7 +1039,7 @@ def test_OffsetFilter_error(mocker, whitelist_conf) -> None:
|
||||
[{"method": "StaticPairList"}, {"method": "OffsetFilter", "offset": -1}]
|
||||
)
|
||||
|
||||
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
|
||||
mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=True))
|
||||
|
||||
with pytest.raises(OperationalException,
|
||||
match=r'OffsetFilter requires offset to be >= 0'):
|
||||
@@ -1039,7 +1050,7 @@ def test_rangestabilityfilter_checks(mocker, default_conf, markets, tickers):
|
||||
default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10},
|
||||
{'method': 'RangeStabilityFilter', 'lookback_days': 99999}]
|
||||
|
||||
mocker.patch.multiple('freqtrade.exchange.Exchange',
|
||||
mocker.patch.multiple(EXMS,
|
||||
markets=PropertyMock(return_value=markets),
|
||||
exchange_has=MagicMock(return_value=True),
|
||||
get_tickers=tickers
|
||||
@@ -1069,7 +1080,7 @@ def test_rangestabilityfilter_caching(mocker, markets, default_conf, tickers, oh
|
||||
'min_rate_of_change': min_rate_of_change,
|
||||
"max_rate_of_change": max_rate_of_change}]
|
||||
|
||||
mocker.patch.multiple('freqtrade.exchange.Exchange',
|
||||
mocker.patch.multiple(EXMS,
|
||||
markets=PropertyMock(return_value=markets),
|
||||
exchange_has=MagicMock(return_value=True),
|
||||
get_tickers=tickers
|
||||
@@ -1083,7 +1094,7 @@ def test_rangestabilityfilter_caching(mocker, markets, default_conf, tickers, oh
|
||||
('BLK/BTC', '1d', CandleType.SPOT): ohlcv_history,
|
||||
}
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
refresh_latest_ohlcv=MagicMock(return_value=ohlcv_data),
|
||||
)
|
||||
|
||||
@@ -1104,7 +1115,7 @@ def test_spreadfilter_invalid_data(mocker, default_conf, markets, tickers, caplo
|
||||
default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10},
|
||||
{'method': 'SpreadFilter', 'max_spread_ratio': 0.1}]
|
||||
|
||||
mocker.patch.multiple('freqtrade.exchange.Exchange',
|
||||
mocker.patch.multiple(EXMS,
|
||||
markets=PropertyMock(return_value=markets),
|
||||
exchange_has=MagicMock(return_value=True),
|
||||
get_tickers=tickers
|
||||
@@ -1118,7 +1129,7 @@ def test_spreadfilter_invalid_data(mocker, default_conf, markets, tickers, caplo
|
||||
tickers.return_value['ETH/BTC']['ask'] = 0.0
|
||||
del tickers.return_value['TKN/BTC']
|
||||
del tickers.return_value['LTC/BTC']
|
||||
mocker.patch.multiple('freqtrade.exchange.Exchange', get_tickers=tickers)
|
||||
mocker.patch.multiple(EXMS, get_tickers=tickers)
|
||||
|
||||
ftbot.pairlists.refresh_pairlist()
|
||||
assert log_has_re(r'Removed .* invalid ticker data.*', caplog)
|
||||
@@ -1192,7 +1203,7 @@ def test_spreadfilter_invalid_data(mocker, default_conf, markets, tickers, caplo
|
||||
])
|
||||
def test_pricefilter_desc(mocker, whitelist_conf, markets, pairlistconfig,
|
||||
desc_expected, exception_expected):
|
||||
mocker.patch.multiple('freqtrade.exchange.Exchange',
|
||||
mocker.patch.multiple(EXMS,
|
||||
markets=PropertyMock(return_value=markets),
|
||||
exchange_has=MagicMock(return_value=True)
|
||||
)
|
||||
@@ -1209,7 +1220,7 @@ def test_pricefilter_desc(mocker, whitelist_conf, markets, pairlistconfig,
|
||||
|
||||
|
||||
def test_pairlistmanager_no_pairlist(mocker, whitelist_conf):
|
||||
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
|
||||
mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=True))
|
||||
|
||||
whitelist_conf['pairlists'] = []
|
||||
|
||||
@@ -1261,14 +1272,14 @@ def test_performance_filter(mocker, whitelist_conf, pairlists, pair_allowlist, o
|
||||
allowlist_conf['pairlists'] = pairlists
|
||||
allowlist_conf['exchange']['pair_whitelist'] = pair_allowlist
|
||||
|
||||
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
|
||||
mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=True))
|
||||
|
||||
freqtrade = get_patched_freqtradebot(mocker, allowlist_conf)
|
||||
mocker.patch.multiple('freqtrade.exchange.Exchange',
|
||||
mocker.patch.multiple(EXMS,
|
||||
get_tickers=tickers,
|
||||
markets=PropertyMock(return_value=markets)
|
||||
)
|
||||
mocker.patch.multiple('freqtrade.exchange.Exchange',
|
||||
mocker.patch.multiple(EXMS,
|
||||
get_historic_ohlcv=MagicMock(return_value=ohlcv_history_list),
|
||||
)
|
||||
mocker.patch.multiple('freqtrade.persistence.Trade',
|
||||
@@ -1366,7 +1377,7 @@ def test_expand_pairlist_keep_invalid(wildcardlist, pairs, expected):
|
||||
|
||||
|
||||
def test_ProducerPairlist_no_emc(mocker, whitelist_conf):
|
||||
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
|
||||
mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=True))
|
||||
|
||||
whitelist_conf['pairlists'] = [
|
||||
{
|
||||
@@ -1383,8 +1394,8 @@ def test_ProducerPairlist_no_emc(mocker, whitelist_conf):
|
||||
|
||||
|
||||
def test_ProducerPairlist(mocker, whitelist_conf, markets):
|
||||
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
|
||||
mocker.patch.multiple('freqtrade.exchange.Exchange',
|
||||
mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=True))
|
||||
mocker.patch.multiple(EXMS,
|
||||
markets=PropertyMock(return_value=markets),
|
||||
exchange_has=MagicMock(return_value=True),
|
||||
)
|
||||
|
@@ -14,7 +14,7 @@ def test_PairLocks(use_db):
|
||||
PairLocks.use_db = use_db
|
||||
# No lock should be present
|
||||
if use_db:
|
||||
assert len(PairLock.query.all()) == 0
|
||||
assert len(PairLock.get_all_locks().all()) == 0
|
||||
|
||||
assert PairLocks.use_db == use_db
|
||||
|
||||
@@ -88,13 +88,13 @@ def test_PairLocks(use_db):
|
||||
|
||||
if use_db:
|
||||
locks = PairLocks.get_all_locks()
|
||||
locks_db = PairLock.query.all()
|
||||
locks_db = PairLock.get_all_locks().all()
|
||||
assert len(locks) == len(locks_db)
|
||||
assert len(locks_db) > 0
|
||||
else:
|
||||
# Nothing was pushed to the database
|
||||
assert len(PairLocks.get_all_locks()) > 0
|
||||
assert len(PairLock.query.all()) == 0
|
||||
assert len(PairLock.get_all_locks().all()) == 0
|
||||
# Reset use-db variable
|
||||
PairLocks.reset_locks()
|
||||
PairLocks.use_db = True
|
||||
@@ -107,7 +107,7 @@ def test_PairLocks_getlongestlock(use_db):
|
||||
# No lock should be present
|
||||
PairLocks.use_db = use_db
|
||||
if use_db:
|
||||
assert len(PairLock.query.all()) == 0
|
||||
assert len(PairLock.get_all_locks().all()) == 0
|
||||
|
||||
assert PairLocks.use_db == use_db
|
||||
|
||||
@@ -139,7 +139,7 @@ def test_PairLocks_reason(use_db):
|
||||
PairLocks.use_db = use_db
|
||||
# No lock should be present
|
||||
if use_db:
|
||||
assert len(PairLock.query.all()) == 0
|
||||
assert len(PairLock.get_all_locks().all()) == 0
|
||||
|
||||
assert PairLocks.use_db == use_db
|
||||
|
||||
|
@@ -39,6 +39,8 @@ def generate_mock_trade(pair: str, fee: float, is_open: bool,
|
||||
order_id=f'{pair}-{trade.entry_side}-{trade.open_date}',
|
||||
ft_is_open=False,
|
||||
ft_pair=pair,
|
||||
ft_amount=trade.amount,
|
||||
ft_price=trade.open_rate,
|
||||
amount=trade.amount,
|
||||
filled=trade.amount,
|
||||
remaining=0,
|
||||
@@ -49,16 +51,19 @@ def generate_mock_trade(pair: str, fee: float, is_open: bool,
|
||||
side=trade.entry_side,
|
||||
))
|
||||
if not is_open:
|
||||
close_price = open_rate * (2 - profit_rate if is_short else profit_rate)
|
||||
trade.orders.append(Order(
|
||||
ft_order_side=trade.exit_side,
|
||||
order_id=f'{pair}-{trade.exit_side}-{trade.close_date}',
|
||||
ft_is_open=False,
|
||||
ft_pair=pair,
|
||||
ft_amount=trade.amount,
|
||||
ft_price=trade.open_rate,
|
||||
amount=trade.amount,
|
||||
filled=trade.amount,
|
||||
remaining=0,
|
||||
price=open_rate * (2 - profit_rate if is_short else profit_rate),
|
||||
average=open_rate * (2 - profit_rate if is_short else profit_rate),
|
||||
price=close_price,
|
||||
average=close_price,
|
||||
status="closed",
|
||||
order_type="market",
|
||||
side=trade.exit_side,
|
||||
@@ -66,10 +71,10 @@ def generate_mock_trade(pair: str, fee: float, is_open: bool,
|
||||
|
||||
trade.recalc_open_trade_value()
|
||||
if not is_open:
|
||||
trade.close(open_rate * (2 - profit_rate if is_short else profit_rate))
|
||||
trade.close(close_price)
|
||||
trade.exit_reason = exit_reason
|
||||
|
||||
Trade.query.session.add(trade)
|
||||
Trade.session.add(trade)
|
||||
Trade.commit()
|
||||
return trade
|
||||
|
||||
|
185
tests/plugins/test_remotepairlist.py
Normal file
185
tests/plugins/test_remotepairlist.py
Normal file
@@ -0,0 +1,185 @@
|
||||
import json
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.plugins.pairlist.RemotePairList import RemotePairList
|
||||
from freqtrade.plugins.pairlistmanager import PairListManager
|
||||
from tests.conftest import get_patched_exchange, get_patched_freqtradebot, log_has
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def rpl_config(default_conf):
|
||||
default_conf['stake_currency'] = 'USDT'
|
||||
|
||||
default_conf['exchange']['pair_whitelist'] = [
|
||||
'ETH/USDT',
|
||||
'BTC/USDT',
|
||||
]
|
||||
default_conf['exchange']['pair_blacklist'] = [
|
||||
'BLK/USDT'
|
||||
]
|
||||
return default_conf
|
||||
|
||||
|
||||
def test_gen_pairlist_with_local_file(mocker, rpl_config):
|
||||
|
||||
mock_file = MagicMock()
|
||||
mock_file.read.return_value = '{"pairs": ["TKN/USDT","ETH/USDT","NANO/USDT"]}'
|
||||
mocker.patch('freqtrade.plugins.pairlist.RemotePairList.open', return_value=mock_file)
|
||||
|
||||
mock_file_path = mocker.patch('freqtrade.plugins.pairlist.RemotePairList.Path')
|
||||
mock_file_path.exists.return_value = True
|
||||
|
||||
jsonparse = json.loads(mock_file.read.return_value)
|
||||
mocker.patch('freqtrade.plugins.pairlist.RemotePairList.json.load', return_value=jsonparse)
|
||||
|
||||
rpl_config['pairlists'] = [
|
||||
{
|
||||
"method": "RemotePairList",
|
||||
'number_assets': 2,
|
||||
'refresh_period': 1800,
|
||||
'keep_pairlist_on_failure': True,
|
||||
'pairlist_url': 'file:///pairlist.json',
|
||||
'bearer_token': '',
|
||||
'read_timeout': 60
|
||||
}
|
||||
]
|
||||
|
||||
exchange = get_patched_exchange(mocker, rpl_config)
|
||||
pairlistmanager = PairListManager(exchange, rpl_config)
|
||||
|
||||
remote_pairlist = RemotePairList(exchange, pairlistmanager, rpl_config,
|
||||
rpl_config['pairlists'][0], 0)
|
||||
|
||||
result = remote_pairlist.gen_pairlist([])
|
||||
|
||||
assert result == ['TKN/USDT', 'ETH/USDT']
|
||||
|
||||
|
||||
def test_fetch_pairlist_mock_response_html(mocker, rpl_config):
|
||||
mock_response = MagicMock()
|
||||
mock_response.headers = {'content-type': 'text/html'}
|
||||
|
||||
rpl_config['pairlists'] = [
|
||||
{
|
||||
"method": "RemotePairList",
|
||||
"pairlist_url": "http://example.com/pairlist",
|
||||
"number_assets": 10,
|
||||
"read_timeout": 10,
|
||||
"keep_pairlist_on_failure": True,
|
||||
}
|
||||
]
|
||||
|
||||
exchange = get_patched_exchange(mocker, rpl_config)
|
||||
pairlistmanager = PairListManager(exchange, rpl_config)
|
||||
|
||||
mocker.patch("freqtrade.plugins.pairlist.RemotePairList.requests.get",
|
||||
return_value=mock_response)
|
||||
remote_pairlist = RemotePairList(exchange, pairlistmanager, rpl_config,
|
||||
rpl_config['pairlists'][0], 0)
|
||||
|
||||
with pytest.raises(OperationalException, match='RemotePairList is not of type JSON, abort.'):
|
||||
remote_pairlist.fetch_pairlist()
|
||||
|
||||
|
||||
def test_fetch_pairlist_timeout_keep_last_pairlist(mocker, rpl_config, caplog):
|
||||
rpl_config['pairlists'] = [
|
||||
{
|
||||
"method": "RemotePairList",
|
||||
"pairlist_url": "http://example.com/pairlist",
|
||||
"number_assets": 10,
|
||||
"read_timeout": 10,
|
||||
"keep_pairlist_on_failure": True,
|
||||
}
|
||||
]
|
||||
|
||||
exchange = get_patched_exchange(mocker, rpl_config)
|
||||
pairlistmanager = PairListManager(exchange, rpl_config)
|
||||
|
||||
mocker.patch("freqtrade.plugins.pairlist.RemotePairList.requests.get",
|
||||
side_effect=requests.exceptions.RequestException)
|
||||
|
||||
remote_pairlist = RemotePairList(exchange, pairlistmanager, rpl_config,
|
||||
rpl_config['pairlists'][0], 0)
|
||||
|
||||
remote_pairlist._last_pairlist = ["BTC/USDT", "ETH/USDT", "LTC/USDT"]
|
||||
|
||||
pairs, time_elapsed = remote_pairlist.fetch_pairlist()
|
||||
assert log_has(f"Was not able to fetch pairlist from: {remote_pairlist._pairlist_url}", caplog)
|
||||
assert log_has("Keeping last fetched pairlist", caplog)
|
||||
assert pairs == ["BTC/USDT", "ETH/USDT", "LTC/USDT"]
|
||||
|
||||
|
||||
def test_remote_pairlist_init_no_pairlist_url(mocker, rpl_config):
|
||||
|
||||
rpl_config['pairlists'] = [
|
||||
{
|
||||
"method": "RemotePairList",
|
||||
"number_assets": 10,
|
||||
"keep_pairlist_on_failure": True,
|
||||
}
|
||||
]
|
||||
|
||||
get_patched_exchange(mocker, rpl_config)
|
||||
with pytest.raises(OperationalException, match=r'`pairlist_url` not specified.'
|
||||
r' Please check your configuration for "pairlist.config.pairlist_url"'):
|
||||
get_patched_freqtradebot(mocker, rpl_config)
|
||||
|
||||
|
||||
def test_remote_pairlist_init_no_number_assets(mocker, rpl_config):
|
||||
|
||||
rpl_config['pairlists'] = [
|
||||
{
|
||||
"method": "RemotePairList",
|
||||
"pairlist_url": "http://example.com/pairlist",
|
||||
"keep_pairlist_on_failure": True,
|
||||
}
|
||||
]
|
||||
|
||||
get_patched_exchange(mocker, rpl_config)
|
||||
|
||||
with pytest.raises(OperationalException, match=r'`number_assets` not specified. '
|
||||
'Please check your configuration for "pairlist.config.number_assets"'):
|
||||
get_patched_freqtradebot(mocker, rpl_config)
|
||||
|
||||
|
||||
def test_fetch_pairlist_mock_response_valid(mocker, rpl_config):
|
||||
|
||||
rpl_config['pairlists'] = [
|
||||
{
|
||||
"method": "RemotePairList",
|
||||
"pairlist_url": "http://example.com/pairlist",
|
||||
"number_assets": 10,
|
||||
"refresh_period": 10,
|
||||
"read_timeout": 10,
|
||||
"keep_pairlist_on_failure": True,
|
||||
}
|
||||
]
|
||||
|
||||
mock_response = MagicMock()
|
||||
|
||||
mock_response.json.return_value = {
|
||||
"pairs": ["ETH/USDT", "XRP/USDT", "LTC/USDT", "EOS/USDT"],
|
||||
"refresh_period": 60
|
||||
}
|
||||
|
||||
mock_response.headers = {
|
||||
"content-type": "application/json"
|
||||
}
|
||||
|
||||
mock_response.elapsed.total_seconds.return_value = 0.4
|
||||
mocker.patch("freqtrade.plugins.pairlist.RemotePairList.requests.get",
|
||||
return_value=mock_response)
|
||||
|
||||
exchange = get_patched_exchange(mocker, rpl_config)
|
||||
pairlistmanager = PairListManager(exchange, rpl_config)
|
||||
remote_pairlist = RemotePairList(exchange, pairlistmanager, rpl_config,
|
||||
rpl_config['pairlists'][0], 0)
|
||||
pairs, time_elapsed = remote_pairlist.fetch_pairlist()
|
||||
|
||||
assert pairs == ["ETH/USDT", "XRP/USDT", "LTC/USDT", "EOS/USDT"]
|
||||
assert time_elapsed == 0.4
|
||||
assert remote_pairlist._refresh_period == 60
|
@@ -1,12 +1,10 @@
|
||||
# pragma pylint: disable=missing-docstring, C0103
|
||||
# pragma pylint: disable=invalid-sequence-index, invalid-name, too-many-arguments
|
||||
|
||||
from copy import deepcopy
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from unittest.mock import ANY, MagicMock, PropertyMock
|
||||
|
||||
import pytest
|
||||
from numpy import isnan
|
||||
from sqlalchemy import select
|
||||
|
||||
from freqtrade.edge import PairInfo
|
||||
from freqtrade.enums import SignalDirection, State, TradingMode
|
||||
@@ -15,19 +13,10 @@ from freqtrade.persistence import Trade
|
||||
from freqtrade.persistence.pairlock_middleware import PairLocks
|
||||
from freqtrade.rpc import RPC, RPCException
|
||||
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
|
||||
from tests.conftest import (create_mock_trades, create_mock_trades_usdt, get_patched_freqtradebot,
|
||||
patch_get_signal)
|
||||
from tests.conftest import (EXMS, create_mock_trades, create_mock_trades_usdt,
|
||||
get_patched_freqtradebot, patch_get_signal)
|
||||
|
||||
|
||||
# Functions for recurrent object patching
|
||||
def prec_satoshi(a, b) -> float:
|
||||
"""
|
||||
:return: True if A and B differs less than one satoshi.
|
||||
"""
|
||||
return abs(a - b) < 0.00000001
|
||||
|
||||
|
||||
# Unit tests
|
||||
def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
||||
gen_response = {
|
||||
'trade_id': 1,
|
||||
@@ -46,13 +35,11 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
||||
'open_rate_requested': ANY,
|
||||
'open_trade_value': 0.0010025,
|
||||
'close_rate_requested': ANY,
|
||||
'sell_reason': ANY,
|
||||
'exit_reason': ANY,
|
||||
'exit_order_status': ANY,
|
||||
'min_rate': ANY,
|
||||
'max_rate': ANY,
|
||||
'strategy': ANY,
|
||||
'buy_tag': ANY,
|
||||
'enter_tag': ANY,
|
||||
'timeframe': 5,
|
||||
'open_order_id': ANY,
|
||||
@@ -64,14 +51,12 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
||||
'amount': 91.07468123,
|
||||
'amount_requested': 91.07468124,
|
||||
'stake_amount': 0.001,
|
||||
'max_stake_amount': None,
|
||||
'trade_duration': None,
|
||||
'trade_duration_s': None,
|
||||
'close_profit': None,
|
||||
'close_profit_pct': None,
|
||||
'close_profit_abs': None,
|
||||
'current_profit': -0.00408133,
|
||||
'current_profit_pct': -0.41,
|
||||
'current_profit_abs': -4.09e-06,
|
||||
'profit_ratio': -0.00408133,
|
||||
'profit_pct': -0.41,
|
||||
'profit_abs': -4.09e-06,
|
||||
@@ -92,6 +77,10 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
||||
'stoploss_entry_dist_ratio': -0.10376381,
|
||||
'open_order': None,
|
||||
'realized_profit': 0.0,
|
||||
'realized_profit_ratio': None,
|
||||
'total_profit_abs': -4.09e-06,
|
||||
'total_profit_fiat': ANY,
|
||||
'total_profit_ratio': None,
|
||||
'exchange': 'binance',
|
||||
'leverage': 1.0,
|
||||
'interest_rate': 0.0,
|
||||
@@ -110,10 +99,10 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
||||
}
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
fetch_ticker=ticker,
|
||||
get_fee=fee,
|
||||
_is_dry_limit_order_filled=MagicMock(side_effect=[False, True]),
|
||||
_dry_is_price_crossed=MagicMock(side_effect=[False, True]),
|
||||
)
|
||||
|
||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||
@@ -135,9 +124,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
||||
'profit_ratio': 0.0,
|
||||
'profit_pct': 0.0,
|
||||
'profit_abs': 0.0,
|
||||
'current_profit': 0.0,
|
||||
'current_profit_pct': 0.0,
|
||||
'current_profit_abs': 0.0,
|
||||
'total_profit_abs': 0.0,
|
||||
'stop_loss_abs': 0.0,
|
||||
'stop_loss_pct': None,
|
||||
'stop_loss_ratio': None,
|
||||
@@ -183,12 +170,16 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
||||
results = rpc._rpc_trade_status()
|
||||
|
||||
response = deepcopy(gen_response)
|
||||
response.update({
|
||||
'max_stake_amount': 0.001,
|
||||
'total_profit_ratio': pytest.approx(-0.00409),
|
||||
})
|
||||
assert results[0] == response
|
||||
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_rate',
|
||||
mocker.patch(f'{EXMS}.get_rate',
|
||||
MagicMock(side_effect=ExchangeError("Pair 'ETH/BTC' not available")))
|
||||
results = rpc._rpc_trade_status()
|
||||
assert isnan(results[0]['current_profit'])
|
||||
assert isnan(results[0]['profit_ratio'])
|
||||
assert isnan(results[0]['current_rate'])
|
||||
response_norate = deepcopy(gen_response)
|
||||
# Update elements that are NaN when no rate is available.
|
||||
@@ -196,12 +187,12 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
||||
'stoploss_current_dist': ANY,
|
||||
'stoploss_current_dist_ratio': ANY,
|
||||
'stoploss_current_dist_pct': ANY,
|
||||
'max_stake_amount': 0.001,
|
||||
'profit_ratio': ANY,
|
||||
'profit_pct': ANY,
|
||||
'profit_abs': ANY,
|
||||
'current_profit_abs': ANY,
|
||||
'current_profit': ANY,
|
||||
'current_profit_pct': ANY,
|
||||
'total_profit_abs': ANY,
|
||||
'total_profit_ratio': ANY,
|
||||
'current_rate': ANY,
|
||||
})
|
||||
assert results[0] == response_norate
|
||||
@@ -215,7 +206,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None:
|
||||
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
fetch_ticker=ticker,
|
||||
get_fee=fee,
|
||||
)
|
||||
@@ -227,7 +218,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None:
|
||||
freqtradebot.state = State.RUNNING
|
||||
with pytest.raises(RPCException, match=r'.*no active trade*'):
|
||||
rpc._rpc_status_table(default_conf['stake_currency'], 'USD')
|
||||
mocker.patch('freqtrade.exchange.Exchange._is_dry_limit_order_filled', return_value=False)
|
||||
mocker.patch(f'{EXMS}._dry_is_price_crossed', return_value=False)
|
||||
freqtradebot.enter_positions()
|
||||
|
||||
result, headers, fiat_profit_sum = rpc._rpc_status_table(default_conf['stake_currency'], 'USD')
|
||||
@@ -238,7 +229,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None:
|
||||
assert '0.00' == result[0][3]
|
||||
assert isnan(fiat_profit_sum)
|
||||
|
||||
mocker.patch('freqtrade.exchange.Exchange._is_dry_limit_order_filled', return_value=True)
|
||||
mocker.patch(f'{EXMS}._dry_is_price_crossed', return_value=True)
|
||||
freqtradebot.process()
|
||||
|
||||
result, headers, fiat_profit_sum = rpc._rpc_status_table(default_conf['stake_currency'], 'USD')
|
||||
@@ -249,7 +240,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None:
|
||||
assert '-0.41%' == result[0][3]
|
||||
assert isnan(fiat_profit_sum)
|
||||
|
||||
# Test with fiatconvert
|
||||
# Test with fiat convert
|
||||
rpc._fiat_converter = CryptoToFiatConverter()
|
||||
result, headers, fiat_profit_sum = rpc._rpc_status_table(default_conf['stake_currency'], 'USD')
|
||||
assert "Since" in headers
|
||||
@@ -269,7 +260,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None:
|
||||
# 3 on top of the initial one.
|
||||
assert result[0][4] == '1/4'
|
||||
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_rate',
|
||||
mocker.patch(f'{EXMS}.get_rate',
|
||||
MagicMock(side_effect=ExchangeError("Pair 'ETH/BTC' not available")))
|
||||
result, headers, fiat_profit_sum = rpc._rpc_status_table(default_conf['stake_currency'], 'USD')
|
||||
assert 'instantly' == result[0][2]
|
||||
@@ -282,7 +273,7 @@ def test__rpc_timeunit_profit(default_conf_usdt, ticker, fee,
|
||||
limit_buy_order, limit_sell_order, markets, mocker) -> None:
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
fetch_ticker=ticker,
|
||||
get_fee=fee,
|
||||
markets=PropertyMock(return_value=markets)
|
||||
@@ -323,7 +314,7 @@ def test__rpc_timeunit_profit(default_conf_usdt, ticker, fee,
|
||||
def test_rpc_trade_history(mocker, default_conf, markets, fee, is_short):
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
markets=PropertyMock(return_value=markets)
|
||||
)
|
||||
|
||||
@@ -351,7 +342,7 @@ def test_rpc_delete_trade(mocker, default_conf, fee, markets, caplog, is_short):
|
||||
stoploss_mock = MagicMock()
|
||||
cancel_mock = MagicMock()
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
markets=PropertyMock(return_value=markets),
|
||||
cancel_order=cancel_mock,
|
||||
cancel_stoploss_order=stoploss_mock,
|
||||
@@ -364,7 +355,7 @@ def test_rpc_delete_trade(mocker, default_conf, fee, markets, caplog, is_short):
|
||||
with pytest.raises(RPCException, match='invalid argument'):
|
||||
rpc._rpc_delete('200')
|
||||
|
||||
trades = Trade.query.all()
|
||||
trades = Trade.session.scalars(select(Trade)).all()
|
||||
trades[1].stoploss_order_id = '1234'
|
||||
trades[2].stoploss_order_id = '1234'
|
||||
assert len(trades) > 2
|
||||
@@ -385,15 +376,13 @@ def test_rpc_delete_trade(mocker, default_conf, fee, markets, caplog, is_short):
|
||||
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)
|
||||
stoploss_mock = mocker.patch(f'{EXMS}.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)
|
||||
cancel_mock = mocker.patch(f'{EXMS}.cancel_order', side_effect=InvalidOrderException)
|
||||
|
||||
res = rpc._rpc_delete('4')
|
||||
assert cancel_mock.call_count == 1
|
||||
@@ -404,7 +393,7 @@ def test_rpc_trade_statistics(default_conf_usdt, ticker, fee, mocker) -> None:
|
||||
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=1.1)
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
fetch_ticker=ticker,
|
||||
get_fee=fee,
|
||||
)
|
||||
@@ -441,7 +430,7 @@ def test_rpc_trade_statistics(default_conf_usdt, ticker, fee, mocker) -> None:
|
||||
assert stats['best_rate'] == 10.0
|
||||
|
||||
# Test non-available pair
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_rate',
|
||||
mocker.patch(f'{EXMS}.get_rate',
|
||||
MagicMock(side_effect=ExchangeError("Pair 'XRP/USDT' not available")))
|
||||
stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
|
||||
assert stats['trade_count'] == 7
|
||||
@@ -475,7 +464,7 @@ def test_rpc_balance_handle_error(default_conf, mocker):
|
||||
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
get_balances=MagicMock(return_value=mock_balance),
|
||||
get_tickers=MagicMock(side_effect=TemporaryError('Could not load ticker due to xxx'))
|
||||
)
|
||||
@@ -538,7 +527,7 @@ def test_rpc_balance_handle(default_conf, mocker, tickers):
|
||||
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
validate_trading_mode_and_margin_mode=MagicMock(),
|
||||
get_balances=MagicMock(return_value=mock_balance),
|
||||
fetch_positions=MagicMock(return_value=mock_pos),
|
||||
@@ -554,8 +543,8 @@ def test_rpc_balance_handle(default_conf, mocker, tickers):
|
||||
rpc._fiat_converter = CryptoToFiatConverter()
|
||||
|
||||
result = rpc._rpc_balance(default_conf['stake_currency'], default_conf['fiat_display_currency'])
|
||||
assert prec_satoshi(result['total'], 30.30909624)
|
||||
assert prec_satoshi(result['value'], 454636.44360691)
|
||||
assert pytest.approx(result['total']) == 30.30909624
|
||||
assert pytest.approx(result['value']) == 454636.44360691
|
||||
assert tickers.call_count == 1
|
||||
assert tickers.call_args_list[0][1]['cached'] is True
|
||||
assert 'USD' == result['symbol']
|
||||
@@ -615,7 +604,7 @@ def test_rpc_balance_handle(default_conf, mocker, tickers):
|
||||
def test_rpc_start(mocker, default_conf) -> None:
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
fetch_ticker=MagicMock()
|
||||
)
|
||||
|
||||
@@ -636,7 +625,7 @@ def test_rpc_start(mocker, default_conf) -> None:
|
||||
def test_rpc_stop(mocker, default_conf) -> None:
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
fetch_ticker=MagicMock()
|
||||
)
|
||||
|
||||
@@ -658,7 +647,7 @@ def test_rpc_stop(mocker, default_conf) -> None:
|
||||
def test_rpc_stopentry(mocker, default_conf) -> None:
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
fetch_ticker=MagicMock()
|
||||
)
|
||||
|
||||
@@ -678,7 +667,7 @@ def test_rpc_force_exit(default_conf, ticker, fee, mocker) -> None:
|
||||
|
||||
cancel_order_mock = MagicMock()
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
fetch_ticker=ticker,
|
||||
cancel_order=cancel_order_mock,
|
||||
fetch_order=MagicMock(
|
||||
@@ -689,7 +678,7 @@ def test_rpc_force_exit(default_conf, ticker, fee, mocker) -> None:
|
||||
'filled': 0.0,
|
||||
}
|
||||
),
|
||||
_is_dry_limit_order_filled=MagicMock(return_value=True),
|
||||
_dry_is_price_crossed=MagicMock(return_value=True),
|
||||
get_fee=fee,
|
||||
)
|
||||
mocker.patch('freqtrade.wallets.Wallets.get_free', return_value=1000)
|
||||
@@ -726,15 +715,14 @@ def test_rpc_force_exit(default_conf, ticker, fee, mocker) -> None:
|
||||
|
||||
freqtradebot.state = State.RUNNING
|
||||
assert cancel_order_mock.call_count == 0
|
||||
mocker.patch(
|
||||
'freqtrade.exchange.Exchange._is_dry_limit_order_filled', MagicMock(return_value=False))
|
||||
mocker.patch(f'{EXMS}._dry_is_price_crossed', MagicMock(return_value=False))
|
||||
freqtradebot.enter_positions()
|
||||
# make an limit-buy open trade
|
||||
trade = Trade.query.filter(Trade.id == '3').first()
|
||||
trade = Trade.session.scalars(select(Trade).filter(Trade.id == '3')).first()
|
||||
filled_amount = trade.amount / 2
|
||||
# Fetch order - it's open first, and closed after cancel_order is called.
|
||||
mocker.patch(
|
||||
'freqtrade.exchange.Exchange.fetch_order',
|
||||
f'{EXMS}.fetch_order',
|
||||
side_effect=[{
|
||||
'id': trade.orders[0].order_id,
|
||||
'status': 'open',
|
||||
@@ -756,7 +744,7 @@ def test_rpc_force_exit(default_conf, ticker, fee, mocker) -> None:
|
||||
assert pytest.approx(trade.amount) == filled_amount
|
||||
|
||||
mocker.patch(
|
||||
'freqtrade.exchange.Exchange.fetch_order',
|
||||
f'{EXMS}.fetch_order',
|
||||
return_value={
|
||||
'status': 'open',
|
||||
'type': 'limit',
|
||||
@@ -766,11 +754,11 @@ def test_rpc_force_exit(default_conf, ticker, fee, mocker) -> None:
|
||||
|
||||
freqtradebot.config['max_open_trades'] = 3
|
||||
freqtradebot.enter_positions()
|
||||
trade = Trade.query.filter(Trade.id == '2').first()
|
||||
trade = Trade.session.scalars(select(Trade).filter(Trade.id == '2')).first()
|
||||
amount = trade.amount
|
||||
# make an limit-buy open trade, if there is no 'filled', don't sell it
|
||||
mocker.patch(
|
||||
'freqtrade.exchange.Exchange.fetch_order',
|
||||
f'{EXMS}.fetch_order',
|
||||
return_value={
|
||||
'status': 'open',
|
||||
'type': 'limit',
|
||||
@@ -784,11 +772,11 @@ def test_rpc_force_exit(default_conf, ticker, fee, mocker) -> None:
|
||||
assert cancel_order_mock.call_count == 2
|
||||
assert trade.amount == amount
|
||||
|
||||
trade = Trade.query.filter(Trade.id == '3').first()
|
||||
trade = Trade.session.scalars(select(Trade).filter(Trade.id == '3')).first()
|
||||
|
||||
# make an limit-sell open trade
|
||||
mocker.patch(
|
||||
'freqtrade.exchange.Exchange.fetch_order',
|
||||
f'{EXMS}.fetch_order',
|
||||
return_value={
|
||||
'status': 'open',
|
||||
'type': 'limit',
|
||||
@@ -808,7 +796,7 @@ def test_rpc_force_exit(default_conf, ticker, fee, mocker) -> None:
|
||||
def test_performance_handle(default_conf_usdt, ticker, fee, mocker) -> None:
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
get_balances=MagicMock(return_value=ticker),
|
||||
fetch_ticker=ticker,
|
||||
get_fee=fee,
|
||||
@@ -831,7 +819,7 @@ def test_enter_tag_performance_handle(default_conf, ticker, fee, mocker) -> None
|
||||
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
get_balances=MagicMock(return_value=ticker),
|
||||
fetch_ticker=ticker,
|
||||
get_fee=fee,
|
||||
@@ -863,7 +851,7 @@ def test_enter_tag_performance_handle(default_conf, ticker, fee, mocker) -> None
|
||||
def test_enter_tag_performance_handle_2(mocker, default_conf, markets, fee):
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
markets=PropertyMock(return_value=markets)
|
||||
)
|
||||
|
||||
@@ -876,23 +864,23 @@ def test_enter_tag_performance_handle_2(mocker, default_conf, markets, fee):
|
||||
assert len(res) == 2
|
||||
assert res[0]['enter_tag'] == 'TEST1'
|
||||
assert res[0]['count'] == 1
|
||||
assert prec_satoshi(res[0]['profit_pct'], 0.5)
|
||||
assert pytest.approx(res[0]['profit_pct']) == 0.5
|
||||
assert res[1]['enter_tag'] == 'Other'
|
||||
assert res[1]['count'] == 1
|
||||
assert prec_satoshi(res[1]['profit_pct'], 1.0)
|
||||
assert pytest.approx(res[1]['profit_pct']) == 1.0
|
||||
|
||||
# Test for a specific pair
|
||||
res = rpc._rpc_enter_tag_performance('ETC/BTC')
|
||||
assert len(res) == 1
|
||||
assert res[0]['count'] == 1
|
||||
assert res[0]['enter_tag'] == 'TEST1'
|
||||
assert prec_satoshi(res[0]['profit_pct'], 0.5)
|
||||
assert pytest.approx(res[0]['profit_pct']) == 0.5
|
||||
|
||||
|
||||
def test_exit_reason_performance_handle(default_conf_usdt, ticker, fee, mocker) -> None:
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
get_balances=MagicMock(return_value=ticker),
|
||||
fetch_ticker=ticker,
|
||||
get_fee=fee,
|
||||
@@ -919,7 +907,7 @@ def test_exit_reason_performance_handle(default_conf_usdt, ticker, fee, mocker)
|
||||
def test_exit_reason_performance_handle_2(mocker, default_conf, markets, fee):
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
markets=PropertyMock(return_value=markets)
|
||||
)
|
||||
|
||||
@@ -932,23 +920,23 @@ def test_exit_reason_performance_handle_2(mocker, default_conf, markets, fee):
|
||||
assert len(res) == 2
|
||||
assert res[0]['exit_reason'] == 'sell_signal'
|
||||
assert res[0]['count'] == 1
|
||||
assert prec_satoshi(res[0]['profit_pct'], 0.5)
|
||||
assert pytest.approx(res[0]['profit_pct']) == 0.5
|
||||
assert res[1]['exit_reason'] == 'roi'
|
||||
assert res[1]['count'] == 1
|
||||
assert prec_satoshi(res[1]['profit_pct'], 1.0)
|
||||
assert pytest.approx(res[1]['profit_pct']) == 1.0
|
||||
|
||||
# Test for a specific pair
|
||||
res = rpc._rpc_exit_reason_performance('ETC/BTC')
|
||||
assert len(res) == 1
|
||||
assert res[0]['count'] == 1
|
||||
assert res[0]['exit_reason'] == 'sell_signal'
|
||||
assert prec_satoshi(res[0]['profit_pct'], 0.5)
|
||||
assert pytest.approx(res[0]['profit_pct']) == 0.5
|
||||
|
||||
|
||||
def test_mix_tag_performance_handle(default_conf, ticker, fee, mocker) -> None:
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
get_balances=MagicMock(return_value=ticker),
|
||||
fetch_ticker=ticker,
|
||||
get_fee=fee,
|
||||
@@ -972,7 +960,7 @@ def test_mix_tag_performance_handle(default_conf, ticker, fee, mocker) -> None:
|
||||
def test_mix_tag_performance_handle_2(mocker, default_conf, markets, fee):
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
markets=PropertyMock(return_value=markets)
|
||||
)
|
||||
|
||||
@@ -985,10 +973,10 @@ def test_mix_tag_performance_handle_2(mocker, default_conf, markets, fee):
|
||||
assert len(res) == 2
|
||||
assert res[0]['mix_tag'] == 'TEST1 sell_signal'
|
||||
assert res[0]['count'] == 1
|
||||
assert prec_satoshi(res[0]['profit_pct'], 0.5)
|
||||
assert pytest.approx(res[0]['profit_pct']) == 0.5
|
||||
assert res[1]['mix_tag'] == 'Other roi'
|
||||
assert res[1]['count'] == 1
|
||||
assert prec_satoshi(res[1]['profit_pct'], 1.0)
|
||||
assert pytest.approx(res[1]['profit_pct']) == 1.0
|
||||
|
||||
# Test for a specific pair
|
||||
res = rpc._rpc_mix_tag_performance('ETC/BTC')
|
||||
@@ -996,13 +984,13 @@ def test_mix_tag_performance_handle_2(mocker, default_conf, markets, fee):
|
||||
assert len(res) == 1
|
||||
assert res[0]['count'] == 1
|
||||
assert res[0]['mix_tag'] == 'TEST1 sell_signal'
|
||||
assert prec_satoshi(res[0]['profit_pct'], 0.5)
|
||||
assert pytest.approx(res[0]['profit_pct']) == 0.5
|
||||
|
||||
|
||||
def test_rpc_count(mocker, default_conf, ticker, fee) -> None:
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
get_balances=MagicMock(return_value=ticker),
|
||||
fetch_ticker=ticker,
|
||||
get_fee=fee,
|
||||
@@ -1027,7 +1015,7 @@ def test_rpc_force_entry(mocker, default_conf, ticker, fee, limit_buy_order_open
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||
buy_mm = MagicMock(return_value=limit_buy_order_open)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
get_balances=MagicMock(return_value=ticker),
|
||||
fetch_ticker=ticker,
|
||||
get_fee=fee,
|
||||
@@ -1156,7 +1144,7 @@ def test_rpc_whitelist_dynamic(mocker, default_conf) -> None:
|
||||
default_conf['pairlists'] = [{'method': 'VolumePairList',
|
||||
'number_assets': 4,
|
||||
}]
|
||||
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
|
||||
mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=True))
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||
|
||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||
@@ -1253,6 +1241,6 @@ def test_rpc_health(mocker, default_conf) -> None:
|
||||
|
||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||
rpc = RPC(freqtradebot)
|
||||
result = rpc._health()
|
||||
assert result['last_process'] == '1970-01-01 00:00:00+00:00'
|
||||
assert result['last_process_ts'] == 0
|
||||
result = rpc.health()
|
||||
assert result['last_process'] is None
|
||||
assert result['last_process_ts'] is None
|
||||
|
@@ -1,8 +1,6 @@
|
||||
"""
|
||||
Unit test file for rpc/api_server.py
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from datetime import datetime, timedelta, timezone
|
||||
@@ -16,6 +14,7 @@ from fastapi import FastAPI, WebSocketDisconnect
|
||||
from fastapi.exceptions import HTTPException
|
||||
from fastapi.testclient import TestClient
|
||||
from requests.auth import _basic_auth_str
|
||||
from sqlalchemy import select
|
||||
|
||||
from freqtrade.__init__ import __version__
|
||||
from freqtrade.enums import CandleType, RunMode, State, TradingMode
|
||||
@@ -26,7 +25,7 @@ from freqtrade.rpc import RPC
|
||||
from freqtrade.rpc.api_server import ApiServer
|
||||
from freqtrade.rpc.api_server.api_auth import create_token, get_user_from_token
|
||||
from freqtrade.rpc.api_server.uvicorn_threaded import UvicornServer
|
||||
from tests.conftest import (CURRENT_TEST_STRATEGY, create_mock_trades, get_mock_coro,
|
||||
from tests.conftest import (CURRENT_TEST_STRATEGY, EXMS, create_mock_trades, get_mock_coro,
|
||||
get_patched_freqtradebot, log_has, log_has_re, patch_get_signal)
|
||||
|
||||
|
||||
@@ -68,22 +67,23 @@ def botclient(default_conf, mocker):
|
||||
ApiServer.shutdown()
|
||||
|
||||
|
||||
def client_post(client, url, data={}):
|
||||
def client_post(client: TestClient, url, data={}):
|
||||
|
||||
return client.post(url,
|
||||
content=data,
|
||||
json=data,
|
||||
headers={'Authorization': _basic_auth_str(_TEST_USER, _TEST_PASS),
|
||||
'Origin': 'http://example.com',
|
||||
'content-type': 'application/json'
|
||||
})
|
||||
|
||||
|
||||
def client_get(client, url):
|
||||
def client_get(client: TestClient, url):
|
||||
# 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 client_delete(client, url):
|
||||
def client_delete(client: TestClient, 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'})
|
||||
@@ -474,9 +474,9 @@ def test_api_balance(botclient, mocker, rpc_balance, tickers):
|
||||
ftbot, client = botclient
|
||||
|
||||
ftbot.config['dry_run'] = False
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=rpc_balance)
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_tickers', tickers)
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_valid_pair_combination',
|
||||
mocker.patch(f'{EXMS}.get_balances', return_value=rpc_balance)
|
||||
mocker.patch(f'{EXMS}.get_tickers', tickers)
|
||||
mocker.patch(f'{EXMS}.get_valid_pair_combination',
|
||||
side_effect=lambda a, b: f"{a}/{b}")
|
||||
ftbot.wallets.update()
|
||||
|
||||
@@ -508,7 +508,7 @@ def test_api_count(botclient, mocker, ticker, fee, markets, is_short):
|
||||
ftbot, client = botclient
|
||||
patch_get_signal(ftbot)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
get_balances=MagicMock(return_value=ticker),
|
||||
fetch_ticker=ticker,
|
||||
get_fee=fee,
|
||||
@@ -561,7 +561,7 @@ def test_api_locks(botclient):
|
||||
assert rc.json()['lock_count'] == 1
|
||||
|
||||
rc = client_post(client, f"{BASE_URI}/locks/delete",
|
||||
data='{"pair": "XRP/BTC"}')
|
||||
data={"pair": "XRP/BTC"})
|
||||
assert_response(rc)
|
||||
assert rc.json()['lock_count'] == 0
|
||||
|
||||
@@ -595,7 +595,7 @@ def test_api_daily(botclient, mocker, ticker, fee, markets):
|
||||
ftbot, client = botclient
|
||||
patch_get_signal(ftbot)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
get_balances=MagicMock(return_value=ticker),
|
||||
fetch_ticker=ticker,
|
||||
get_fee=fee,
|
||||
@@ -614,7 +614,7 @@ def test_api_trades(botclient, mocker, fee, markets, is_short):
|
||||
ftbot, client = botclient
|
||||
patch_get_signal(ftbot)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
markets=PropertyMock(return_value=markets)
|
||||
)
|
||||
rc = client_get(client, f"{BASE_URI}/trades")
|
||||
@@ -625,7 +625,7 @@ def test_api_trades(botclient, mocker, fee, markets, is_short):
|
||||
assert rc.json()['offset'] == 0
|
||||
|
||||
create_mock_trades(fee, is_short=is_short)
|
||||
Trade.query.session.flush()
|
||||
Trade.session.flush()
|
||||
|
||||
rc = client_get(client, f"{BASE_URI}/trades")
|
||||
assert_response(rc)
|
||||
@@ -645,7 +645,7 @@ def test_api_trade_single(botclient, mocker, fee, ticker, markets, is_short):
|
||||
ftbot, client = botclient
|
||||
patch_get_signal(ftbot, enter_long=not is_short, enter_short=is_short)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
markets=PropertyMock(return_value=markets),
|
||||
fetch_ticker=ticker,
|
||||
)
|
||||
@@ -653,7 +653,7 @@ def test_api_trade_single(botclient, mocker, fee, ticker, markets, is_short):
|
||||
assert_response(rc, 404)
|
||||
assert rc.json()['detail'] == 'Trade not found.'
|
||||
|
||||
Trade.query.session.rollback()
|
||||
Trade.rollback()
|
||||
create_mock_trades(fee, is_short=is_short)
|
||||
|
||||
rc = client_get(client, f"{BASE_URI}/trade/3")
|
||||
@@ -669,7 +669,7 @@ def test_api_delete_trade(botclient, mocker, fee, markets, is_short):
|
||||
stoploss_mock = MagicMock()
|
||||
cancel_mock = MagicMock()
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
markets=PropertyMock(return_value=markets),
|
||||
cancel_order=cancel_mock,
|
||||
cancel_stoploss_order=stoploss_mock,
|
||||
@@ -678,7 +678,7 @@ def test_api_delete_trade(botclient, mocker, fee, markets, is_short):
|
||||
create_mock_trades(fee, is_short=is_short)
|
||||
|
||||
ftbot.strategy.order_types['stoploss_on_exchange'] = True
|
||||
trades = Trade.query.all()
|
||||
trades = Trade.session.scalars(select(Trade)).all()
|
||||
trades[1].stoploss_order_id = '1234'
|
||||
Trade.commit()
|
||||
assert len(trades) > 2
|
||||
@@ -686,7 +686,7 @@ def test_api_delete_trade(botclient, mocker, fee, markets, is_short):
|
||||
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 len(trades) - 1 == len(Trade.session.scalars(select(Trade)).all())
|
||||
assert cancel_mock.call_count == 1
|
||||
|
||||
cancel_mock.reset_mock()
|
||||
@@ -695,11 +695,11 @@ def test_api_delete_trade(botclient, mocker, fee, markets, is_short):
|
||||
assert_response(rc, 502)
|
||||
assert cancel_mock.call_count == 0
|
||||
|
||||
assert len(trades) - 1 == len(Trade.query.all())
|
||||
assert len(trades) - 1 == len(Trade.session.scalars(select(Trade)).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 len(trades) - 2 == len(Trade.session.scalars(select(Trade)).all())
|
||||
assert stoploss_mock.call_count == 1
|
||||
|
||||
rc = client_delete(client, f"{BASE_URI}/trades/502")
|
||||
@@ -707,6 +707,44 @@ def test_api_delete_trade(botclient, mocker, fee, markets, is_short):
|
||||
assert_response(rc, 502)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('is_short', [True, False])
|
||||
def test_api_delete_open_order(botclient, mocker, fee, markets, ticker, is_short):
|
||||
ftbot, client = botclient
|
||||
patch_get_signal(ftbot, enter_long=not is_short, enter_short=is_short)
|
||||
stoploss_mock = MagicMock()
|
||||
cancel_mock = MagicMock()
|
||||
mocker.patch.multiple(
|
||||
EXMS,
|
||||
markets=PropertyMock(return_value=markets),
|
||||
fetch_ticker=ticker,
|
||||
cancel_order=cancel_mock,
|
||||
cancel_stoploss_order=stoploss_mock,
|
||||
)
|
||||
|
||||
rc = client_delete(client, f"{BASE_URI}/trades/10/open-order")
|
||||
assert_response(rc, 502)
|
||||
assert 'Invalid trade_id.' in rc.json()['error']
|
||||
|
||||
create_mock_trades(fee, is_short=is_short)
|
||||
Trade.commit()
|
||||
|
||||
rc = client_delete(client, f"{BASE_URI}/trades/5/open-order")
|
||||
assert_response(rc, 502)
|
||||
assert 'No open order for trade_id' in rc.json()['error']
|
||||
trade = Trade.get_trades([Trade.id == 6]).first()
|
||||
mocker.patch(f'{EXMS}.fetch_order', side_effect=ExchangeError)
|
||||
rc = client_delete(client, f"{BASE_URI}/trades/6/open-order")
|
||||
assert_response(rc, 502)
|
||||
assert 'Order not found.' in rc.json()['error']
|
||||
|
||||
trade = Trade.get_trades([Trade.id == 6]).first()
|
||||
mocker.patch(f'{EXMS}.fetch_order', return_value=trade.orders[-1].to_ccxt_object())
|
||||
|
||||
rc = client_delete(client, f"{BASE_URI}/trades/6/open-order")
|
||||
assert_response(rc)
|
||||
assert cancel_mock.call_count == 1
|
||||
|
||||
|
||||
def test_api_logs(botclient):
|
||||
ftbot, client = botclient
|
||||
rc = client_get(client, f"{BASE_URI}/logs")
|
||||
@@ -743,7 +781,7 @@ def test_api_edge_disabled(botclient, mocker, ticker, fee, markets):
|
||||
ftbot, client = botclient
|
||||
patch_get_signal(ftbot)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
get_balances=MagicMock(return_value=ticker),
|
||||
fetch_ticker=ticker,
|
||||
get_fee=fee,
|
||||
@@ -805,7 +843,7 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, is_short, expected)
|
||||
ftbot, client = botclient
|
||||
patch_get_signal(ftbot)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
get_balances=MagicMock(return_value=ticker),
|
||||
fetch_ticker=ticker,
|
||||
get_fee=fee,
|
||||
@@ -863,7 +901,7 @@ def test_api_stats(botclient, mocker, ticker, fee, markets, is_short):
|
||||
ftbot, client = botclient
|
||||
patch_get_signal(ftbot, enter_long=not is_short, enter_short=is_short)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
get_balances=MagicMock(return_value=ticker),
|
||||
fetch_ticker=ticker,
|
||||
get_fee=fee,
|
||||
@@ -906,7 +944,7 @@ def test_api_performance(botclient, fee):
|
||||
)
|
||||
trade.close_profit = trade.calc_profit_ratio(trade.close_rate)
|
||||
trade.close_profit_abs = trade.calc_profit(trade.close_rate)
|
||||
Trade.query.session.add(trade)
|
||||
Trade.session.add(trade)
|
||||
|
||||
trade = Trade(
|
||||
pair='XRP/ETH',
|
||||
@@ -923,7 +961,7 @@ def test_api_performance(botclient, fee):
|
||||
trade.close_profit = trade.calc_profit_ratio(trade.close_rate)
|
||||
trade.close_profit_abs = trade.calc_profit(trade.close_rate)
|
||||
|
||||
Trade.query.session.add(trade)
|
||||
Trade.session.add(trade)
|
||||
Trade.commit()
|
||||
|
||||
rc = client_get(client, f"{BASE_URI}/performance")
|
||||
@@ -944,7 +982,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets, is_short,
|
||||
ftbot, client = botclient
|
||||
patch_get_signal(ftbot)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
get_balances=MagicMock(return_value=ticker),
|
||||
fetch_ticker=ticker,
|
||||
get_fee=fee,
|
||||
@@ -969,13 +1007,15 @@ def test_api_status(botclient, mocker, ticker, fee, markets, is_short,
|
||||
'close_profit_pct': None,
|
||||
'close_profit_abs': None,
|
||||
'close_rate': None,
|
||||
'current_profit': ANY,
|
||||
'current_profit_pct': ANY,
|
||||
'current_profit_abs': ANY,
|
||||
'profit_ratio': ANY,
|
||||
'profit_pct': ANY,
|
||||
'profit_abs': ANY,
|
||||
'profit_fiat': ANY,
|
||||
'total_profit_abs': ANY,
|
||||
'total_profit_fiat': ANY,
|
||||
'total_profit_ratio': ANY,
|
||||
'realized_profit': 0.0,
|
||||
'realized_profit_ratio': None,
|
||||
'current_rate': current_rate,
|
||||
'open_date': ANY,
|
||||
'open_timestamp': ANY,
|
||||
@@ -985,6 +1025,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets, is_short,
|
||||
'base_currency': 'ETH',
|
||||
'quote_currency': 'BTC',
|
||||
'stake_amount': 0.001,
|
||||
'max_stake_amount': ANY,
|
||||
'stop_loss_abs': ANY,
|
||||
'stop_loss_pct': ANY,
|
||||
'stop_loss_ratio': ANY,
|
||||
@@ -1014,11 +1055,9 @@ def test_api_status(botclient, mocker, ticker, fee, markets, is_short,
|
||||
'open_order_id': open_order_id,
|
||||
'open_rate_requested': ANY,
|
||||
'open_trade_value': open_trade_value,
|
||||
'sell_reason': None,
|
||||
'exit_reason': None,
|
||||
'exit_order_status': None,
|
||||
'strategy': CURRENT_TEST_STRATEGY,
|
||||
'buy_tag': None,
|
||||
'enter_tag': None,
|
||||
'timeframe': 5,
|
||||
'exchange': 'binance',
|
||||
@@ -1030,7 +1069,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets, is_short,
|
||||
'orders': [ANY],
|
||||
}
|
||||
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_rate',
|
||||
mocker.patch(f'{EXMS}.get_rate',
|
||||
MagicMock(side_effect=ExchangeError("Pair 'ETH/BTC' not available")))
|
||||
|
||||
rc = client_get(client, f"{BASE_URI}/status")
|
||||
@@ -1063,7 +1102,7 @@ def test_api_blacklist(botclient, mocker):
|
||||
|
||||
# Add ETH/BTC to blacklist
|
||||
rc = client_post(client, f"{BASE_URI}/blacklist",
|
||||
data='{"blacklist": ["ETH/BTC"]}')
|
||||
data={"blacklist": ["ETH/BTC"]})
|
||||
assert_response(rc)
|
||||
assert rc.json() == {"blacklist": ["DOGE/BTC", "HOT/BTC", "ETH/BTC"],
|
||||
"blacklist_expanded": ["ETH/BTC"],
|
||||
@@ -1073,7 +1112,7 @@ def test_api_blacklist(botclient, mocker):
|
||||
}
|
||||
|
||||
rc = client_post(client, f"{BASE_URI}/blacklist",
|
||||
data='{"blacklist": ["XRP/.*"]}')
|
||||
data={"blacklist": ["XRP/.*"]})
|
||||
assert_response(rc)
|
||||
assert rc.json() == {"blacklist": ["DOGE/BTC", "HOT/BTC", "ETH/BTC", "XRP/.*"],
|
||||
"blacklist_expanded": ["ETH/BTC", "XRP/BTC", "XRP/USDT"],
|
||||
@@ -1135,7 +1174,7 @@ def test_api_force_entry(botclient, mocker, fee, endpoint):
|
||||
ftbot, client = botclient
|
||||
|
||||
rc = client_post(client, f"{BASE_URI}/{endpoint}",
|
||||
data='{"pair": "ETH/BTC"}')
|
||||
data={"pair": "ETH/BTC"})
|
||||
assert_response(rc, 502)
|
||||
assert rc.json() == {"error": f"Error querying /api/v1/{endpoint}: Force_entry not enabled."}
|
||||
|
||||
@@ -1143,9 +1182,9 @@ def test_api_force_entry(botclient, mocker, fee, endpoint):
|
||||
ftbot.config['force_entry_enable'] = True
|
||||
|
||||
fbuy_mock = MagicMock(return_value=None)
|
||||
mocker.patch("freqtrade.rpc.RPC._rpc_force_entry", fbuy_mock)
|
||||
mocker.patch("freqtrade.rpc.rpc.RPC._rpc_force_entry", fbuy_mock)
|
||||
rc = client_post(client, f"{BASE_URI}/{endpoint}",
|
||||
data='{"pair": "ETH/BTC"}')
|
||||
data={"pair": "ETH/BTC"})
|
||||
assert_response(rc)
|
||||
assert rc.json() == {"status": "Error entering long trade for pair ETH/BTC."}
|
||||
|
||||
@@ -1169,10 +1208,10 @@ def test_api_force_entry(botclient, mocker, fee, endpoint):
|
||||
strategy=CURRENT_TEST_STRATEGY,
|
||||
trading_mode=TradingMode.SPOT
|
||||
))
|
||||
mocker.patch("freqtrade.rpc.RPC._rpc_force_entry", fbuy_mock)
|
||||
mocker.patch("freqtrade.rpc.rpc.RPC._rpc_force_entry", fbuy_mock)
|
||||
|
||||
rc = client_post(client, f"{BASE_URI}/{endpoint}",
|
||||
data='{"pair": "ETH/BTC"}')
|
||||
data={"pair": "ETH/BTC"})
|
||||
assert_response(rc)
|
||||
assert rc.json() == {
|
||||
'amount': 1.0,
|
||||
@@ -1188,6 +1227,7 @@ def test_api_force_entry(botclient, mocker, fee, endpoint):
|
||||
'base_currency': 'ETH',
|
||||
'quote_currency': 'BTC',
|
||||
'stake_amount': 1,
|
||||
'max_stake_amount': ANY,
|
||||
'stop_loss_abs': None,
|
||||
'stop_loss_pct': None,
|
||||
'stop_loss_ratio': None,
|
||||
@@ -1205,6 +1245,8 @@ def test_api_force_entry(botclient, mocker, fee, endpoint):
|
||||
'profit_pct': None,
|
||||
'profit_abs': None,
|
||||
'profit_fiat': None,
|
||||
'realized_profit': 0.0,
|
||||
'realized_profit_ratio': None,
|
||||
'fee_close': 0.0025,
|
||||
'fee_close_cost': None,
|
||||
'fee_close_currency': None,
|
||||
@@ -1218,11 +1260,9 @@ def test_api_force_entry(botclient, mocker, fee, endpoint):
|
||||
'open_order_id': '123456',
|
||||
'open_rate_requested': None,
|
||||
'open_trade_value': 0.24605460,
|
||||
'sell_reason': None,
|
||||
'exit_reason': None,
|
||||
'exit_order_status': None,
|
||||
'strategy': CURRENT_TEST_STRATEGY,
|
||||
'buy_tag': None,
|
||||
'enter_tag': None,
|
||||
'timeframe': 5,
|
||||
'exchange': 'binance',
|
||||
@@ -1238,39 +1278,39 @@ def test_api_force_entry(botclient, mocker, fee, endpoint):
|
||||
def test_api_forceexit(botclient, mocker, ticker, fee, markets):
|
||||
ftbot, client = botclient
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
get_balances=MagicMock(return_value=ticker),
|
||||
fetch_ticker=ticker,
|
||||
get_fee=fee,
|
||||
markets=PropertyMock(return_value=markets),
|
||||
_is_dry_limit_order_filled=MagicMock(return_value=True),
|
||||
_dry_is_price_crossed=MagicMock(return_value=True),
|
||||
)
|
||||
patch_get_signal(ftbot)
|
||||
|
||||
rc = client_post(client, f"{BASE_URI}/forceexit",
|
||||
data='{"tradeid": "1"}')
|
||||
data={"tradeid": "1"})
|
||||
assert_response(rc, 502)
|
||||
assert rc.json() == {"error": "Error querying /api/v1/forceexit: invalid argument"}
|
||||
Trade.query.session.rollback()
|
||||
Trade.rollback()
|
||||
|
||||
create_mock_trades(fee)
|
||||
trade = Trade.get_trades([Trade.id == 5]).first()
|
||||
assert pytest.approx(trade.amount) == 123
|
||||
rc = client_post(client, f"{BASE_URI}/forceexit",
|
||||
data='{"tradeid": "5", "ordertype": "market", "amount": 23}')
|
||||
data={"tradeid": "5", "ordertype": "market", "amount": 23})
|
||||
assert_response(rc)
|
||||
assert rc.json() == {'result': 'Created sell order for trade 5.'}
|
||||
Trade.query.session.rollback()
|
||||
Trade.rollback()
|
||||
|
||||
trade = Trade.get_trades([Trade.id == 5]).first()
|
||||
assert pytest.approx(trade.amount) == 100
|
||||
assert trade.is_open is True
|
||||
|
||||
rc = client_post(client, f"{BASE_URI}/forceexit",
|
||||
data='{"tradeid": "5"}')
|
||||
data={"tradeid": "5"})
|
||||
assert_response(rc)
|
||||
assert rc.json() == {'result': 'Created sell order for trade 5.'}
|
||||
Trade.query.session.rollback()
|
||||
Trade.rollback()
|
||||
|
||||
trade = Trade.get_trades([Trade.id == 5]).first()
|
||||
assert trade.is_open is False
|
||||
@@ -1420,7 +1460,7 @@ def test_api_pair_history(botclient, ohlcv_history):
|
||||
"No data for UNITTEST/BTC, 5m in 20200111-20200112 found.")
|
||||
|
||||
|
||||
def test_api_plot_config(botclient):
|
||||
def test_api_plot_config(botclient, mocker):
|
||||
ftbot, client = botclient
|
||||
|
||||
rc = client_get(client, f"{BASE_URI}/plot_config")
|
||||
@@ -1444,6 +1484,21 @@ def test_api_plot_config(botclient):
|
||||
assert isinstance(rc.json()['main_plot'], dict)
|
||||
assert isinstance(rc.json()['subplots'], dict)
|
||||
|
||||
rc = client_get(client, f"{BASE_URI}/plot_config?strategy=freqai_test_classifier")
|
||||
assert_response(rc)
|
||||
res = rc.json()
|
||||
assert 'target_roi' in res['subplots']
|
||||
assert 'do_predict' in res['subplots']
|
||||
|
||||
rc = client_get(client, f"{BASE_URI}/plot_config?strategy=HyperoptableStrategy")
|
||||
assert_response(rc)
|
||||
assert rc.json()['subplots'] == {}
|
||||
|
||||
mocker.patch('freqtrade.rpc.api_server.api_v1.get_rpc_optional', return_value=None)
|
||||
|
||||
rc = client_get(client, f"{BASE_URI}/plot_config")
|
||||
assert_response(rc)
|
||||
|
||||
|
||||
def test_api_strategies(botclient, tmpdir):
|
||||
ftbot, client = botclient
|
||||
@@ -1488,6 +1543,44 @@ def test_api_strategy(botclient):
|
||||
assert_response(rc, 500)
|
||||
|
||||
|
||||
def test_api_freqaimodels(botclient, tmpdir, mocker):
|
||||
ftbot, client = botclient
|
||||
ftbot.config['user_data_dir'] = Path(tmpdir)
|
||||
mocker.patch(
|
||||
"freqtrade.resolvers.freqaimodel_resolver.FreqaiModelResolver.search_all_objects",
|
||||
return_value=[
|
||||
{'name': 'LightGBMClassifier'},
|
||||
{'name': 'LightGBMClassifierMultiTarget'},
|
||||
{'name': 'LightGBMRegressor'},
|
||||
{'name': 'LightGBMRegressorMultiTarget'},
|
||||
{'name': 'ReinforcementLearner'},
|
||||
{'name': 'ReinforcementLearner_multiproc'},
|
||||
{'name': 'XGBoostClassifier'},
|
||||
{'name': 'XGBoostRFClassifier'},
|
||||
{'name': 'XGBoostRFRegressor'},
|
||||
{'name': 'XGBoostRegressor'},
|
||||
{'name': 'XGBoostRegressorMultiTarget'},
|
||||
])
|
||||
|
||||
rc = client_get(client, f"{BASE_URI}/freqaimodels")
|
||||
|
||||
assert_response(rc)
|
||||
|
||||
assert rc.json() == {'freqaimodels': [
|
||||
'LightGBMClassifier',
|
||||
'LightGBMClassifierMultiTarget',
|
||||
'LightGBMRegressor',
|
||||
'LightGBMRegressorMultiTarget',
|
||||
'ReinforcementLearner',
|
||||
'ReinforcementLearner_multiproc',
|
||||
'XGBoostClassifier',
|
||||
'XGBoostRFClassifier',
|
||||
'XGBoostRFRegressor',
|
||||
'XGBoostRegressor',
|
||||
'XGBoostRegressorMultiTarget'
|
||||
]}
|
||||
|
||||
|
||||
def test_list_available_pairs(botclient):
|
||||
ftbot, client = botclient
|
||||
|
||||
@@ -1518,13 +1611,13 @@ def test_list_available_pairs(botclient):
|
||||
client, f"{BASE_URI}/available_pairs?timeframe=1h")
|
||||
assert_response(rc)
|
||||
assert rc.json()['length'] == 1
|
||||
assert rc.json()['pairs'] == ['XRP/USDT']
|
||||
assert rc.json()['pairs'] == ['XRP/USDT:USDT']
|
||||
|
||||
rc = client_get(
|
||||
client, f"{BASE_URI}/available_pairs?timeframe=1h&candletype=mark")
|
||||
assert_response(rc)
|
||||
assert rc.json()['length'] == 2
|
||||
assert rc.json()['pairs'] == ['UNITTEST/USDT', 'XRP/USDT']
|
||||
assert rc.json()['pairs'] == ['UNITTEST/USDT:USDT', 'XRP/USDT:USDT']
|
||||
assert len(rc.json()['pair_interval']) == 2
|
||||
|
||||
|
||||
@@ -1540,7 +1633,7 @@ def test_sysinfo(botclient):
|
||||
|
||||
def test_api_backtesting(botclient, mocker, fee, caplog, tmpdir):
|
||||
ftbot, client = botclient
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||
mocker.patch(f'{EXMS}.get_fee', fee)
|
||||
|
||||
rc = client_get(client, f"{BASE_URI}/backtest")
|
||||
# Backtest prevented in default mode
|
||||
@@ -1580,7 +1673,7 @@ def test_api_backtesting(botclient, mocker, fee, caplog, tmpdir):
|
||||
"dry_run_wallet": 1000,
|
||||
"enable_protections": False
|
||||
}
|
||||
rc = client_post(client, f"{BASE_URI}/backtest", data=json.dumps(data))
|
||||
rc = client_post(client, f"{BASE_URI}/backtest", data=data)
|
||||
assert_response(rc)
|
||||
result = rc.json()
|
||||
|
||||
@@ -1631,7 +1724,7 @@ def test_api_backtesting(botclient, mocker, fee, caplog, tmpdir):
|
||||
assert result['status'] == 'running'
|
||||
|
||||
# Post to backtest that's still running
|
||||
rc = client_post(client, f"{BASE_URI}/backtest", data=json.dumps(data))
|
||||
rc = client_post(client, f"{BASE_URI}/backtest", data=data)
|
||||
assert_response(rc, 502)
|
||||
result = rc.json()
|
||||
assert 'Bot Background task already running' in result['error']
|
||||
@@ -1639,7 +1732,7 @@ def test_api_backtesting(botclient, mocker, fee, caplog, tmpdir):
|
||||
ApiServer._bgtask_running = False
|
||||
|
||||
# Rerun backtest (should get previous result)
|
||||
rc = client_post(client, f"{BASE_URI}/backtest", data=json.dumps(data))
|
||||
rc = client_post(client, f"{BASE_URI}/backtest", data=data)
|
||||
assert_response(rc)
|
||||
result = rc.json()
|
||||
assert log_has_re('Reusing result of previous backtest.*', caplog)
|
||||
@@ -1647,9 +1740,15 @@ def test_api_backtesting(botclient, mocker, fee, caplog, tmpdir):
|
||||
data['stake_amount'] = 101
|
||||
|
||||
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest_one_strategy',
|
||||
side_effect=DependencyException())
|
||||
rc = client_post(client, f"{BASE_URI}/backtest", data=json.dumps(data))
|
||||
assert log_has("Backtesting caused an error: ", caplog)
|
||||
side_effect=DependencyException('DeadBeef'))
|
||||
rc = client_post(client, f"{BASE_URI}/backtest", data=data)
|
||||
assert log_has("Backtesting caused an error: DeadBeef", caplog)
|
||||
|
||||
rc = client_get(client, f"{BASE_URI}/backtest")
|
||||
assert_response(rc)
|
||||
result = rc.json()
|
||||
assert result['status'] == 'error'
|
||||
assert 'Backtest failed' in result['status_msg']
|
||||
|
||||
# Delete backtesting to avoid leakage since the backtest-object may stick around.
|
||||
rc = client_delete(client, f"{BASE_URI}/backtest")
|
||||
@@ -1662,7 +1761,7 @@ def test_api_backtesting(botclient, mocker, fee, caplog, tmpdir):
|
||||
|
||||
# Disallow base64 strategies
|
||||
data['strategy'] = "xx:cHJpbnQoImhlbGxvIHdvcmxkIik="
|
||||
rc = client_post(client, f"{BASE_URI}/backtest", data=json.dumps(data))
|
||||
rc = client_post(client, f"{BASE_URI}/backtest", data=data)
|
||||
assert_response(rc, 500)
|
||||
|
||||
|
||||
@@ -1671,7 +1770,7 @@ def test_api_backtest_history(botclient, mocker, testdatadir):
|
||||
mocker.patch('freqtrade.data.btanalysis._get_backtest_files',
|
||||
return_value=[
|
||||
testdatadir / 'backtest_results/backtest-result_multistrat.json',
|
||||
testdatadir / 'backtest_results/backtest-result_new.json'
|
||||
testdatadir / 'backtest_results/backtest-result.json'
|
||||
])
|
||||
|
||||
rc = client_get(client, f"{BASE_URI}/backtest/history")
|
||||
@@ -1705,8 +1804,8 @@ def test_health(botclient):
|
||||
|
||||
assert_response(rc)
|
||||
ret = rc.json()
|
||||
assert ret['last_process_ts'] == 0
|
||||
assert ret['last_process'] == '1970-01-01T00:00:00+00:00'
|
||||
assert ret["last_process_ts"] is None
|
||||
assert ret["last_process"] is None
|
||||
|
||||
|
||||
def test_api_ws_subscribe(botclient, mocker):
|
||||
@@ -1730,7 +1829,7 @@ def test_api_ws_subscribe(botclient, mocker):
|
||||
assert sub_mock.call_count == 1
|
||||
|
||||
|
||||
def test_api_ws_requests(botclient, mocker, caplog):
|
||||
def test_api_ws_requests(botclient, caplog):
|
||||
caplog.set_level(logging.DEBUG)
|
||||
|
||||
ftbot, client = botclient
|
||||
|
@@ -83,6 +83,7 @@ def test_emc_init(patched_emc):
|
||||
def test_emc_handle_producer_message(patched_emc, caplog, ohlcv_history):
|
||||
test_producer = {"name": "test", "url": "ws://test", "ws_token": "test"}
|
||||
producer_name = test_producer['name']
|
||||
invalid_msg = r"Invalid message .+"
|
||||
|
||||
caplog.set_level(logging.DEBUG)
|
||||
|
||||
@@ -94,7 +95,7 @@ def test_emc_handle_producer_message(patched_emc, caplog, ohlcv_history):
|
||||
assert log_has(
|
||||
f"Consumed message from `{producer_name}` of type `RPCMessageType.WHITELIST`", caplog)
|
||||
|
||||
# Test handle analyzed_df message
|
||||
# Test handle analyzed_df single candle message
|
||||
df_message = {
|
||||
"type": "analyzed_df",
|
||||
"data": {
|
||||
@@ -106,8 +107,7 @@ def test_emc_handle_producer_message(patched_emc, caplog, ohlcv_history):
|
||||
patched_emc.handle_producer_message(test_producer, df_message)
|
||||
|
||||
assert log_has(f"Received message of type `analyzed_df` from `{producer_name}`", caplog)
|
||||
assert log_has(
|
||||
f"Consumed message from `{producer_name}` of type `RPCMessageType.ANALYZED_DF`", caplog)
|
||||
assert log_has_re(r"Holes in data or no existing df,.+", caplog)
|
||||
|
||||
# Test unhandled message
|
||||
unhandled_message = {"type": "status", "data": "RUNNING"}
|
||||
@@ -120,7 +120,8 @@ def test_emc_handle_producer_message(patched_emc, caplog, ohlcv_history):
|
||||
malformed_message = {"type": "whitelist", "data": {"pair": "BTC/USDT"}}
|
||||
patched_emc.handle_producer_message(test_producer, malformed_message)
|
||||
|
||||
assert log_has_re(r"Invalid message .+", caplog)
|
||||
assert log_has_re(invalid_msg, caplog)
|
||||
caplog.clear()
|
||||
|
||||
malformed_message = {
|
||||
"type": "analyzed_df",
|
||||
@@ -133,13 +134,30 @@ def test_emc_handle_producer_message(patched_emc, caplog, ohlcv_history):
|
||||
patched_emc.handle_producer_message(test_producer, malformed_message)
|
||||
|
||||
assert log_has(f"Received message of type `analyzed_df` from `{producer_name}`", caplog)
|
||||
assert log_has_re(r"Invalid message .+", caplog)
|
||||
assert log_has_re(invalid_msg, caplog)
|
||||
caplog.clear()
|
||||
|
||||
# Empty dataframe
|
||||
malformed_message = {
|
||||
"type": "analyzed_df",
|
||||
"data": {
|
||||
"key": ("BTC/USDT", "5m", "spot"),
|
||||
"df": ohlcv_history.loc[ohlcv_history['open'] < 0],
|
||||
"la": datetime.now(timezone.utc)
|
||||
}
|
||||
}
|
||||
patched_emc.handle_producer_message(test_producer, malformed_message)
|
||||
|
||||
assert log_has(f"Received message of type `analyzed_df` from `{producer_name}`", caplog)
|
||||
assert not log_has_re(invalid_msg, caplog)
|
||||
assert log_has_re(r"Received Empty Dataframe for.+", caplog)
|
||||
|
||||
caplog.clear()
|
||||
malformed_message = {"some": "stuff"}
|
||||
patched_emc.handle_producer_message(test_producer, malformed_message)
|
||||
|
||||
assert log_has_re(r"Invalid message .+", caplog)
|
||||
assert log_has_re(invalid_msg, caplog)
|
||||
caplog.clear()
|
||||
|
||||
caplog.clear()
|
||||
malformed_message = {"type": "whitelist", "data": None}
|
||||
@@ -183,7 +201,7 @@ async def test_emc_create_connection_success(default_conf, caplog, mocker):
|
||||
async with websockets.serve(eat, _TEST_WS_HOST, _TEST_WS_PORT):
|
||||
await emc._create_connection(test_producer, lock)
|
||||
|
||||
assert log_has_re(r"Producer connection success.+", caplog)
|
||||
assert log_has_re(r"Connected to channel.+", caplog)
|
||||
finally:
|
||||
emc.shutdown()
|
||||
|
||||
@@ -212,7 +230,8 @@ async def test_emc_create_connection_invalid_url(default_conf, caplog, mocker, h
|
||||
|
||||
dp = DataProvider(default_conf, None, None, None)
|
||||
# Handle start explicitly to avoid messing with threading in tests
|
||||
mocker.patch("freqtrade.rpc.external_message_consumer.ExternalMessageConsumer.start",)
|
||||
mocker.patch("freqtrade.rpc.external_message_consumer.ExternalMessageConsumer.start")
|
||||
mocker.patch("freqtrade.rpc.api_server.ws.channel.create_channel")
|
||||
emc = ExternalMessageConsumer(default_conf, dp)
|
||||
|
||||
try:
|
||||
@@ -248,7 +267,7 @@ async def test_emc_create_connection_error(default_conf, caplog, mocker):
|
||||
emc = ExternalMessageConsumer(default_conf, dp)
|
||||
|
||||
try:
|
||||
await asyncio.sleep(0.01)
|
||||
await asyncio.sleep(0.05)
|
||||
assert log_has("Unexpected error has occurred:", caplog)
|
||||
finally:
|
||||
emc.shutdown()
|
||||
@@ -390,7 +409,9 @@ async def test_emc_receive_messages_timeout(default_conf, caplog, mocker):
|
||||
try:
|
||||
change_running(emc)
|
||||
loop.call_soon(functools.partial(change_running, emc=emc))
|
||||
await emc._receive_messages(TestChannel(), test_producer, lock)
|
||||
|
||||
with pytest.raises(asyncio.TimeoutError):
|
||||
await emc._receive_messages(TestChannel(), test_producer, lock)
|
||||
|
||||
assert log_has_re(r"Ping error.+", caplog)
|
||||
finally:
|
||||
|
@@ -14,13 +14,15 @@ import arrow
|
||||
import pytest
|
||||
import time_machine
|
||||
from pandas import DataFrame
|
||||
from sqlalchemy import select
|
||||
from telegram import Chat, Message, ReplyKeyboardMarkup, Update
|
||||
from telegram.error import BadRequest, NetworkError, TelegramError
|
||||
|
||||
from freqtrade import __version__
|
||||
from freqtrade.constants import CANCEL_REASON
|
||||
from freqtrade.edge import PairInfo
|
||||
from freqtrade.enums import ExitType, RPCMessageType, RunMode, SignalDirection, State
|
||||
from freqtrade.enums import (ExitType, MarketDirection, RPCMessageType, RunMode, SignalDirection,
|
||||
State)
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.freqtradebot import FreqtradeBot
|
||||
from freqtrade.loggers import setup_logging
|
||||
@@ -29,9 +31,9 @@ from freqtrade.persistence.models import Order
|
||||
from freqtrade.rpc import RPC
|
||||
from freqtrade.rpc.rpc import RPCException
|
||||
from freqtrade.rpc.telegram import Telegram, authorized_only
|
||||
from tests.conftest import (CURRENT_TEST_STRATEGY, create_mock_trades, create_mock_trades_usdt,
|
||||
get_patched_freqtradebot, log_has, log_has_re, patch_exchange,
|
||||
patch_get_signal, patch_whitelist)
|
||||
from tests.conftest import (CURRENT_TEST_STRATEGY, EXMS, create_mock_trades,
|
||||
create_mock_trades_usdt, get_patched_freqtradebot, log_has, log_has_re,
|
||||
patch_exchange, patch_get_signal, patch_whitelist)
|
||||
|
||||
|
||||
class DummyCls(Telegram):
|
||||
@@ -99,14 +101,14 @@ def test_telegram_init(default_conf, mocker, caplog) -> None:
|
||||
message_str = ("rpc.telegram is listening for following commands: [['status'], ['profit'], "
|
||||
"['balance'], ['start'], ['stop'], "
|
||||
"['forcesell', 'forceexit', 'fx'], ['forcebuy', 'forcelong'], ['forceshort'], "
|
||||
"['trades'], ['delete'], ['performance'], "
|
||||
"['trades'], ['delete'], ['coo', 'cancel_open_order'], ['performance'], "
|
||||
"['buys', 'entries'], ['sells', 'exits'], ['mix_tags'], "
|
||||
"['stats'], ['daily'], ['weekly'], ['monthly'], "
|
||||
"['count'], ['locks'], ['unlock', 'delete_locks'], "
|
||||
"['reload_config', 'reload_conf'], ['show_config', 'show_conf'], "
|
||||
"['stopbuy', 'stopentry'], ['whitelist'], ['blacklist'], "
|
||||
"['blacklist_delete', 'bl_delete'], "
|
||||
"['logs'], ['edge'], ['health'], ['help'], ['version']"
|
||||
"['logs'], ['edge'], ['health'], ['help'], ['version'], ['marketdir']"
|
||||
"]")
|
||||
|
||||
assert log_has(message_str, caplog)
|
||||
@@ -197,11 +199,15 @@ def test_telegram_status(default_conf, update, mocker) -> None:
|
||||
'current_rate': 1.098e-05,
|
||||
'amount': 90.99181074,
|
||||
'stake_amount': 90.99181074,
|
||||
'max_stake_amount': 90.99181074,
|
||||
'buy_tag': None,
|
||||
'enter_tag': None,
|
||||
'close_profit_ratio': None,
|
||||
'profit': -0.0059,
|
||||
'profit_ratio': -0.0059,
|
||||
'profit_abs': -0.225,
|
||||
'realized_profit': 0.0,
|
||||
'total_profit_abs': -0.225,
|
||||
'initial_stop_loss_abs': 1.098e-05,
|
||||
'stop_loss_abs': 1.099e-05,
|
||||
'exit_order_status': None,
|
||||
@@ -236,7 +242,7 @@ def test_telegram_status_multi_entry(default_conf, update, mocker, fee) -> None:
|
||||
default_conf['telegram']['chat_id'] = "123"
|
||||
default_conf['position_adjustment_enable'] = True
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
fetch_order=MagicMock(return_value=None),
|
||||
get_rate=MagicMock(return_value=0.22),
|
||||
)
|
||||
@@ -253,6 +259,8 @@ def test_telegram_status_multi_entry(default_conf, update, mocker, fee) -> None:
|
||||
ft_order_side='buy',
|
||||
ft_pair=trade.pair,
|
||||
ft_is_open=False,
|
||||
ft_amount=trade.amount,
|
||||
ft_price=trade.open_rate,
|
||||
status="closed",
|
||||
symbol=trade.pair,
|
||||
order_type="market",
|
||||
@@ -273,6 +281,7 @@ def test_telegram_status_multi_entry(default_conf, update, mocker, fee) -> None:
|
||||
assert msg_mock.call_count == 4
|
||||
msg = msg_mock.call_args_list[0][0][0]
|
||||
assert re.search(r'Number of Entries.*2', msg)
|
||||
assert re.search(r'Number of Exits.*0', msg)
|
||||
assert re.search(r'Average Entry Price', msg)
|
||||
assert re.search(r'Order filled', msg)
|
||||
assert re.search(r'Close Date:', msg) is None
|
||||
@@ -286,7 +295,7 @@ def test_telegram_status_closed_trade(default_conf, update, mocker, fee) -> None
|
||||
default_conf['telegram']['chat_id'] = "123"
|
||||
default_conf['position_adjustment_enable'] = True
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
fetch_order=MagicMock(return_value=None),
|
||||
get_rate=MagicMock(return_value=0.22),
|
||||
)
|
||||
@@ -294,8 +303,7 @@ def test_telegram_status_closed_trade(default_conf, update, mocker, fee) -> None
|
||||
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
|
||||
|
||||
create_mock_trades(fee)
|
||||
trades = Trade.get_trades([Trade.is_open.is_(False)])
|
||||
trade = trades[0]
|
||||
trade = Trade.get_trades([Trade.is_open.is_(False)]).first()
|
||||
context = MagicMock()
|
||||
context.args = [str(trade.id)]
|
||||
telegram._status(update=update, context=context)
|
||||
@@ -308,10 +316,10 @@ def test_telegram_status_closed_trade(default_conf, update, mocker, fee) -> None
|
||||
def test_status_handle(default_conf, update, ticker, fee, mocker) -> None:
|
||||
default_conf['max_open_trades'] = 3
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
fetch_ticker=ticker,
|
||||
get_fee=fee,
|
||||
_is_dry_limit_order_filled=MagicMock(return_value=True),
|
||||
_dry_is_price_crossed=MagicMock(return_value=True),
|
||||
)
|
||||
status_table = MagicMock()
|
||||
mocker.patch.multiple(
|
||||
@@ -385,7 +393,7 @@ def test_status_handle(default_conf, update, ticker, fee, mocker) -> None:
|
||||
|
||||
def test_status_table_handle(default_conf, update, ticker, fee, mocker) -> None:
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
fetch_ticker=ticker,
|
||||
get_fee=fee,
|
||||
)
|
||||
@@ -430,7 +438,7 @@ def test_daily_handle(default_conf_usdt, update, ticker, fee, mocker, time_machi
|
||||
return_value=1.1
|
||||
)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
fetch_ticker=ticker,
|
||||
get_fee=fee,
|
||||
)
|
||||
@@ -485,7 +493,7 @@ def test_daily_handle(default_conf_usdt, update, ticker, fee, mocker, time_machi
|
||||
|
||||
def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None:
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
fetch_ticker=ticker
|
||||
)
|
||||
|
||||
@@ -519,7 +527,7 @@ def test_weekly_handle(default_conf_usdt, update, ticker, fee, mocker, time_mach
|
||||
return_value=1.1
|
||||
)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
fetch_ticker=ticker,
|
||||
get_fee=fee,
|
||||
)
|
||||
@@ -589,7 +597,7 @@ def test_monthly_handle(default_conf_usdt, update, ticker, fee, mocker, time_mac
|
||||
return_value=1.1
|
||||
)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
fetch_ticker=ticker,
|
||||
get_fee=fee,
|
||||
)
|
||||
@@ -670,7 +678,7 @@ def test_profit_handle(default_conf_usdt, update, ticker_usdt, ticker_sell_up, f
|
||||
limit_sell_order_usdt, mocker) -> None:
|
||||
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=1.1)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
fetch_ticker=ticker_usdt,
|
||||
get_fee=fee,
|
||||
)
|
||||
@@ -685,7 +693,7 @@ def test_profit_handle(default_conf_usdt, update, ticker_usdt, ticker_sell_up, f
|
||||
|
||||
# Create some test data
|
||||
freqtradebot.enter_positions()
|
||||
trade = Trade.query.first()
|
||||
trade = Trade.session.scalars(select(Trade)).first()
|
||||
|
||||
context = MagicMock()
|
||||
# Test with invalid 2nd argument (should silently pass)
|
||||
@@ -700,7 +708,7 @@ def test_profit_handle(default_conf_usdt, update, ticker_usdt, ticker_sell_up, f
|
||||
msg_mock.reset_mock()
|
||||
|
||||
# Update the ticker with a market going up
|
||||
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', ticker_sell_up)
|
||||
mocker.patch(f'{EXMS}.fetch_ticker', ticker_sell_up)
|
||||
# Simulate fulfilled LIMIT_SELL order for trade
|
||||
oobj = Order.parse_from_ccxt_object(
|
||||
limit_sell_order_usdt, limit_sell_order_usdt['symbol'], 'sell')
|
||||
@@ -733,7 +741,7 @@ def test_profit_handle(default_conf_usdt, update, ticker_usdt, ticker_sell_up, f
|
||||
def test_telegram_stats(default_conf, update, ticker, fee, mocker, is_short) -> None:
|
||||
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
fetch_ticker=ticker,
|
||||
get_fee=fee,
|
||||
)
|
||||
@@ -758,10 +766,9 @@ def test_telegram_stats(default_conf, update, ticker, fee, mocker, is_short) ->
|
||||
|
||||
def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance, tickers) -> None:
|
||||
default_conf['dry_run'] = False
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=rpc_balance)
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_tickers', tickers)
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_valid_pair_combination',
|
||||
side_effect=lambda a, b: f"{a}/{b}")
|
||||
mocker.patch(f'{EXMS}.get_balances', return_value=rpc_balance)
|
||||
mocker.patch(f'{EXMS}.get_tickers', tickers)
|
||||
mocker.patch(f'{EXMS}.get_valid_pair_combination', side_effect=lambda a, b: f"{a}/{b}")
|
||||
|
||||
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
|
||||
patch_get_signal(freqtradebot)
|
||||
@@ -784,7 +791,7 @@ def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance, tick
|
||||
|
||||
def test_balance_handle_empty_response(default_conf, update, mocker) -> None:
|
||||
default_conf['dry_run'] = False
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value={})
|
||||
mocker.patch(f'{EXMS}.get_balances', return_value={})
|
||||
|
||||
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
|
||||
patch_get_signal(freqtradebot)
|
||||
@@ -797,7 +804,7 @@ def test_balance_handle_empty_response(default_conf, update, mocker) -> None:
|
||||
|
||||
|
||||
def test_balance_handle_empty_response_dry(default_conf, update, mocker) -> None:
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value={})
|
||||
mocker.patch(f'{EXMS}.get_balances', return_value={})
|
||||
|
||||
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
|
||||
patch_get_signal(freqtradebot)
|
||||
@@ -925,10 +932,10 @@ def test_telegram_forceexit_handle(default_conf, update, ticker, fee,
|
||||
patch_exchange(mocker)
|
||||
patch_whitelist(mocker, default_conf)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
fetch_ticker=ticker,
|
||||
get_fee=fee,
|
||||
_is_dry_limit_order_filled=MagicMock(return_value=True),
|
||||
_dry_is_price_crossed=MagicMock(return_value=True),
|
||||
)
|
||||
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
@@ -939,11 +946,11 @@ def test_telegram_forceexit_handle(default_conf, update, ticker, fee,
|
||||
# Create some test data
|
||||
freqtradebot.enter_positions()
|
||||
|
||||
trade = Trade.query.first()
|
||||
trade = Trade.session.scalars(select(Trade)).first()
|
||||
assert trade
|
||||
|
||||
# Increase the price and sell it
|
||||
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', ticker_sell_up)
|
||||
mocker.patch(f'{EXMS}.fetch_ticker', ticker_sell_up)
|
||||
|
||||
# /forceexit 1
|
||||
context = MagicMock()
|
||||
@@ -994,10 +1001,10 @@ def test_telegram_force_exit_down_handle(default_conf, update, ticker, fee,
|
||||
patch_whitelist(mocker, default_conf)
|
||||
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
fetch_ticker=ticker,
|
||||
get_fee=fee,
|
||||
_is_dry_limit_order_filled=MagicMock(return_value=True),
|
||||
_dry_is_price_crossed=MagicMock(return_value=True),
|
||||
)
|
||||
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
@@ -1010,11 +1017,11 @@ def test_telegram_force_exit_down_handle(default_conf, update, ticker, fee,
|
||||
|
||||
# Decrease the price and sell it
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
fetch_ticker=ticker_sell_down
|
||||
)
|
||||
|
||||
trade = Trade.query.first()
|
||||
trade = Trade.session.scalars(select(Trade)).first()
|
||||
assert trade
|
||||
|
||||
# /forceexit 1
|
||||
@@ -1065,10 +1072,10 @@ def test_forceexit_all_handle(default_conf, update, ticker, fee, mocker) -> None
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
||||
patch_whitelist(mocker, default_conf)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
fetch_ticker=ticker,
|
||||
get_fee=fee,
|
||||
_is_dry_limit_order_filled=MagicMock(return_value=True),
|
||||
_dry_is_price_crossed=MagicMock(return_value=True),
|
||||
)
|
||||
default_conf['max_open_trades'] = 4
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
@@ -1150,10 +1157,10 @@ def test_forceexit_handle_invalid(default_conf, update, mocker) -> None:
|
||||
def test_force_exit_no_pair(default_conf, update, ticker, fee, mocker) -> None:
|
||||
default_conf['max_open_trades'] = 4
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
fetch_ticker=ticker,
|
||||
get_fee=fee,
|
||||
_is_dry_limit_order_filled=MagicMock(return_value=True),
|
||||
_dry_is_price_crossed=MagicMock(return_value=True),
|
||||
)
|
||||
femock = mocker.patch('freqtrade.rpc.rpc.RPC._rpc_force_exit')
|
||||
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
|
||||
@@ -1204,7 +1211,7 @@ def test_force_enter_handle(default_conf, update, mocker) -> None:
|
||||
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
|
||||
|
||||
fbuy_mock = MagicMock(return_value=None)
|
||||
mocker.patch('freqtrade.rpc.RPC._rpc_force_entry', fbuy_mock)
|
||||
mocker.patch('freqtrade.rpc.rpc.RPC._rpc_force_entry', fbuy_mock)
|
||||
|
||||
telegram, freqtradebot, _ = get_telegram_testobject(mocker, default_conf)
|
||||
patch_get_signal(freqtradebot)
|
||||
@@ -1221,7 +1228,7 @@ def test_force_enter_handle(default_conf, update, mocker) -> None:
|
||||
|
||||
# Reset and retry with specified price
|
||||
fbuy_mock = MagicMock(return_value=None)
|
||||
mocker.patch('freqtrade.rpc.RPC._rpc_force_entry', fbuy_mock)
|
||||
mocker.patch('freqtrade.rpc.rpc.RPC._rpc_force_entry', fbuy_mock)
|
||||
# /forcelong ETH/BTC 0.055
|
||||
context = MagicMock()
|
||||
context.args = ["ETH/BTC", "0.055"]
|
||||
@@ -1250,7 +1257,7 @@ def test_force_enter_no_pair(default_conf, update, mocker) -> None:
|
||||
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
|
||||
|
||||
fbuy_mock = MagicMock(return_value=None)
|
||||
mocker.patch('freqtrade.rpc.RPC._rpc_force_entry', fbuy_mock)
|
||||
mocker.patch('freqtrade.rpc.rpc.RPC._rpc_force_entry', fbuy_mock)
|
||||
|
||||
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
|
||||
|
||||
@@ -1277,7 +1284,7 @@ def test_force_enter_no_pair(default_conf, update, mocker) -> None:
|
||||
def test_telegram_performance_handle(default_conf_usdt, update, ticker, fee, mocker) -> None:
|
||||
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
fetch_ticker=ticker,
|
||||
get_fee=fee,
|
||||
)
|
||||
@@ -1295,7 +1302,7 @@ def test_telegram_performance_handle(default_conf_usdt, update, ticker, fee, moc
|
||||
def test_telegram_entry_tag_performance_handle(
|
||||
default_conf_usdt, update, ticker, fee, mocker) -> None:
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
fetch_ticker=ticker,
|
||||
get_fee=fee,
|
||||
)
|
||||
@@ -1326,7 +1333,7 @@ def test_telegram_entry_tag_performance_handle(
|
||||
def test_telegram_exit_reason_performance_handle(default_conf_usdt, update, ticker, fee,
|
||||
mocker) -> None:
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
fetch_ticker=ticker,
|
||||
get_fee=fee,
|
||||
)
|
||||
@@ -1357,7 +1364,7 @@ def test_telegram_exit_reason_performance_handle(default_conf_usdt, update, tick
|
||||
def test_telegram_mix_tag_performance_handle(default_conf_usdt, update, ticker, fee,
|
||||
mocker) -> None:
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
fetch_ticker=ticker,
|
||||
get_fee=fee,
|
||||
)
|
||||
@@ -1389,7 +1396,7 @@ def test_telegram_mix_tag_performance_handle(default_conf_usdt, update, ticker,
|
||||
|
||||
def test_count_handle(default_conf, update, ticker, fee, mocker) -> None:
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
fetch_ticker=ticker,
|
||||
get_fee=fee,
|
||||
)
|
||||
@@ -1418,7 +1425,7 @@ def test_count_handle(default_conf, update, ticker, fee, mocker) -> None:
|
||||
|
||||
def test_telegram_lock_handle(default_conf, update, ticker, fee, mocker) -> None:
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
fetch_ticker=ticker,
|
||||
get_fee=fee,
|
||||
)
|
||||
@@ -1486,7 +1493,7 @@ def test_whitelist_static(default_conf, update, mocker) -> None:
|
||||
|
||||
|
||||
def test_whitelist_dynamic(default_conf, update, mocker) -> None:
|
||||
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
|
||||
mocker.patch(f'{EXMS}.exchange_has', return_value=True)
|
||||
default_conf['pairlists'] = [{'method': 'VolumePairList',
|
||||
'number_assets': 4
|
||||
}]
|
||||
@@ -1676,6 +1683,39 @@ def test_telegram_delete_trade(mocker, update, default_conf, fee, is_short):
|
||||
assert "Please make sure to take care of this asset" in msg_mock.call_args_list[0][0][0]
|
||||
|
||||
|
||||
@pytest.mark.parametrize('is_short', [True, False])
|
||||
def test_telegram_delete_open_order(mocker, update, default_conf, fee, is_short, ticker):
|
||||
|
||||
mocker.patch.multiple(
|
||||
EXMS,
|
||||
fetch_ticker=ticker,
|
||||
)
|
||||
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
|
||||
context = MagicMock()
|
||||
context.args = []
|
||||
|
||||
telegram._cancel_open_order(update=update, context=context)
|
||||
assert "Trade-id not set." in msg_mock.call_args_list[0][0][0]
|
||||
|
||||
msg_mock.reset_mock()
|
||||
create_mock_trades(fee, is_short=is_short)
|
||||
|
||||
context = MagicMock()
|
||||
context.args = [5]
|
||||
telegram._cancel_open_order(update=update, context=context)
|
||||
assert "No open order for trade_id" in msg_mock.call_args_list[0][0][0]
|
||||
|
||||
msg_mock.reset_mock()
|
||||
|
||||
trade = Trade.get_trades([Trade.id == 6]).first()
|
||||
mocker.patch(f'{EXMS}.fetch_order', return_value=trade.orders[-1].to_ccxt_object())
|
||||
context = MagicMock()
|
||||
context.args = [6]
|
||||
telegram._cancel_open_order(update=update, context=context)
|
||||
assert msg_mock.call_count == 1
|
||||
assert "Open order canceled." in msg_mock.call_args_list[0][0][0]
|
||||
|
||||
|
||||
def test_help_handle(default_conf, update, mocker) -> None:
|
||||
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
|
||||
|
||||
@@ -1973,7 +2013,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
|
||||
'sub_trade': True,
|
||||
})
|
||||
assert msg_mock.call_args[0][0] == (
|
||||
'\N{WARNING SIGN} *Binance (dry):* Exiting KEY/ETH (#1)\n'
|
||||
'\N{WARNING SIGN} *Binance (dry):* Partially exiting KEY/ETH (#1)\n'
|
||||
'*Unrealized Sub Profit:* `-57.41% (loss: -0.05746268 ETH / -24.812 USD)`\n'
|
||||
'*Cumulative Profit:* (`-0.15746268 ETH / -24.812 USD`)\n'
|
||||
'*Enter Tag:* `buy_signal1`\n'
|
||||
@@ -2358,3 +2398,15 @@ def test__send_msg_keyboard(default_conf, mocker, caplog) -> None:
|
||||
assert log_has("using custom keyboard from config.json: "
|
||||
"[['/daily', '/stats', '/balance', '/profit', '/profit 5'], ['/count', "
|
||||
"'/start', '/reload_config', '/help']]", caplog)
|
||||
|
||||
|
||||
def test_change_market_direction(default_conf, mocker, update) -> None:
|
||||
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
|
||||
assert telegram._rpc._freqtrade.strategy.market_direction == MarketDirection.NONE
|
||||
context = MagicMock()
|
||||
context.args = ["long"]
|
||||
telegram._changemarketdir(update, context)
|
||||
assert telegram._rpc._freqtrade.strategy.market_direction == MarketDirection.LONG
|
||||
context = MagicMock()
|
||||
context.args = ["invalid"]
|
||||
assert telegram._rpc._freqtrade.strategy.market_direction == MarketDirection.LONG
|
||||
|
@@ -356,6 +356,14 @@ def test_exception_send_msg(default_conf, mocker, caplog):
|
||||
}
|
||||
webhook.send_msg(msg)
|
||||
|
||||
# Test no failure for not implemented but known messagetypes
|
||||
for e in RPCMessageType:
|
||||
msg = {
|
||||
'type': e,
|
||||
'status': 'whatever'
|
||||
}
|
||||
webhook.send_msg(msg)
|
||||
|
||||
|
||||
def test__send_msg(default_conf, mocker, caplog):
|
||||
default_conf["webhook"] = get_webhook_dict()
|
||||
|
@@ -7,6 +7,7 @@ from datetime import datetime
|
||||
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.persistence.trade_model import Order
|
||||
from freqtrade.strategy.interface import IStrategy
|
||||
|
||||
|
||||
@@ -35,7 +36,7 @@ class TestStrategyImplementBuyTimeout(TestStrategyNoImplementSell):
|
||||
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
return super().populate_exit_trend(dataframe, metadata)
|
||||
|
||||
def check_buy_timeout(self, pair: str, trade, order: dict,
|
||||
def check_buy_timeout(self, pair: str, trade, order: Order,
|
||||
current_time: datetime, **kwargs) -> bool:
|
||||
return False
|
||||
|
||||
@@ -44,6 +45,6 @@ class TestStrategyImplementSellTimeout(TestStrategyNoImplementSell):
|
||||
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
return super().populate_exit_trend(dataframe, metadata)
|
||||
|
||||
def check_sell_timeout(self, pair: str, trade, order: dict,
|
||||
def check_sell_timeout(self, pair: str, trade, order: Order,
|
||||
current_time: datetime, **kwargs) -> bool:
|
||||
return False
|
||||
|
@@ -1,11 +1,11 @@
|
||||
import logging
|
||||
from functools import reduce
|
||||
from typing import Dict
|
||||
|
||||
import pandas as pd
|
||||
import talib.abstract as ta
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.strategy import IStrategy, merge_informative_pair
|
||||
from freqtrade.strategy import IStrategy
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -22,52 +22,40 @@ class freqai_rl_test_strat(IStrategy):
|
||||
process_only_new_candles = True
|
||||
stoploss = -0.05
|
||||
use_exit_signal = True
|
||||
startup_candle_count: int = 30
|
||||
startup_candle_count: int = 300
|
||||
can_short = False
|
||||
|
||||
def populate_any_indicators(
|
||||
self, pair, df, tf, informative=None, set_generalized_indicators=False
|
||||
):
|
||||
def feature_engineering_expand_all(self, dataframe: DataFrame, period: int,
|
||||
metadata: Dict, **kwargs):
|
||||
|
||||
if informative is None:
|
||||
informative = self.dp.get_pair_dataframe(pair, tf)
|
||||
dataframe["%-rsi-period"] = ta.RSI(dataframe, timeperiod=period)
|
||||
|
||||
# first loop is automatically duplicating indicators for time periods
|
||||
for t in self.freqai_info["feature_parameters"]["indicator_periods_candles"]:
|
||||
return dataframe
|
||||
|
||||
t = int(t)
|
||||
informative[f"%-{pair}rsi-period_{t}"] = ta.RSI(informative, timeperiod=t)
|
||||
def feature_engineering_expand_basic(self, dataframe: DataFrame, metadata: Dict, **kwargs):
|
||||
|
||||
# The following columns are necessary for RL models.
|
||||
informative[f"%-{pair}raw_close"] = informative["close"]
|
||||
informative[f"%-{pair}raw_open"] = informative["open"]
|
||||
informative[f"%-{pair}raw_high"] = informative["high"]
|
||||
informative[f"%-{pair}raw_low"] = informative["low"]
|
||||
dataframe["%-pct-change"] = dataframe["close"].pct_change()
|
||||
dataframe["%-raw_volume"] = dataframe["volume"]
|
||||
|
||||
indicators = [col for col in informative if col.startswith("%")]
|
||||
# This loop duplicates and shifts all indicators to add a sense of recency to data
|
||||
for n in range(self.freqai_info["feature_parameters"]["include_shifted_candles"] + 1):
|
||||
if n == 0:
|
||||
continue
|
||||
informative_shift = informative[indicators].shift(n)
|
||||
informative_shift = informative_shift.add_suffix("_shift-" + str(n))
|
||||
informative = pd.concat((informative, informative_shift), axis=1)
|
||||
return dataframe
|
||||
|
||||
df = merge_informative_pair(df, informative, self.config["timeframe"], tf, ffill=True)
|
||||
skip_columns = [
|
||||
(s + "_" + tf) for s in ["date", "open", "high", "low", "close", "volume"]
|
||||
]
|
||||
df = df.drop(columns=skip_columns)
|
||||
def feature_engineering_standard(self, dataframe: DataFrame, metadata: Dict, **kwargs):
|
||||
|
||||
# Add generalized indicators here (because in live, it will call this
|
||||
# function to populate indicators during training). Notice how we ensure not to
|
||||
# add them multiple times
|
||||
if set_generalized_indicators:
|
||||
# For RL, there are no direct targets to set. This is filler (neutral)
|
||||
# until the agent sends an action.
|
||||
df["&-action"] = 0
|
||||
dataframe["%-day_of_week"] = dataframe["date"].dt.dayofweek
|
||||
dataframe["%-hour_of_day"] = dataframe["date"].dt.hour
|
||||
|
||||
return df
|
||||
dataframe["%-raw_close"] = dataframe["close"]
|
||||
dataframe["%-raw_open"] = dataframe["open"]
|
||||
dataframe["%-raw_high"] = dataframe["high"]
|
||||
dataframe["%-raw_low"] = dataframe["low"]
|
||||
|
||||
return dataframe
|
||||
|
||||
def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs):
|
||||
|
||||
dataframe["&-action"] = 0
|
||||
|
||||
return dataframe
|
||||
|
||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
|
||||
|
@@ -1,12 +1,12 @@
|
||||
import logging
|
||||
from functools import reduce
|
||||
from typing import Dict
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import talib.abstract as ta
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.strategy import DecimalParameter, IntParameter, IStrategy, merge_informative_pair
|
||||
from freqtrade.strategy import DecimalParameter, IntParameter, IStrategy
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -57,55 +57,36 @@ class freqai_test_classifier(IStrategy):
|
||||
informative_pairs.append((pair, tf))
|
||||
return informative_pairs
|
||||
|
||||
def populate_any_indicators(
|
||||
self, pair, df, tf, informative=None, set_generalized_indicators=False
|
||||
):
|
||||
def feature_engineering_expand_all(self, dataframe: DataFrame, period: int,
|
||||
metadata: Dict, **kwargs):
|
||||
|
||||
coin = pair.split('/')[0]
|
||||
dataframe["%-rsi-period"] = ta.RSI(dataframe, timeperiod=period)
|
||||
dataframe["%-mfi-period"] = ta.MFI(dataframe, timeperiod=period)
|
||||
dataframe["%-adx-period"] = ta.ADX(dataframe, timeperiod=period)
|
||||
|
||||
if informative is None:
|
||||
informative = self.dp.get_pair_dataframe(pair, tf)
|
||||
return dataframe
|
||||
|
||||
# first loop is automatically duplicating indicators for time periods
|
||||
for t in self.freqai_info["feature_parameters"]["indicator_periods_candles"]:
|
||||
def feature_engineering_expand_basic(self, dataframe: DataFrame, metadata: Dict, **kwargs):
|
||||
|
||||
t = int(t)
|
||||
informative[f"%-{coin}rsi-period_{t}"] = ta.RSI(informative, timeperiod=t)
|
||||
informative[f"%-{coin}mfi-period_{t}"] = ta.MFI(informative, timeperiod=t)
|
||||
informative[f"%-{coin}adx-period_{t}"] = ta.ADX(informative, window=t)
|
||||
dataframe["%-pct-change"] = dataframe["close"].pct_change()
|
||||
dataframe["%-raw_volume"] = dataframe["volume"]
|
||||
dataframe["%-raw_price"] = dataframe["close"]
|
||||
|
||||
informative[f"%-{coin}pct-change"] = informative["close"].pct_change()
|
||||
informative[f"%-{coin}raw_volume"] = informative["volume"]
|
||||
informative[f"%-{coin}raw_price"] = informative["close"]
|
||||
return dataframe
|
||||
|
||||
indicators = [col for col in informative if col.startswith("%")]
|
||||
# This loop duplicates and shifts all indicators to add a sense of recency to data
|
||||
for n in range(self.freqai_info["feature_parameters"]["include_shifted_candles"] + 1):
|
||||
if n == 0:
|
||||
continue
|
||||
informative_shift = informative[indicators].shift(n)
|
||||
informative_shift = informative_shift.add_suffix("_shift-" + str(n))
|
||||
informative = pd.concat((informative, informative_shift), axis=1)
|
||||
def feature_engineering_standard(self, dataframe: DataFrame, metadata: Dict, **kwargs):
|
||||
|
||||
df = merge_informative_pair(df, informative, self.config["timeframe"], tf, ffill=True)
|
||||
skip_columns = [
|
||||
(s + "_" + tf) for s in ["date", "open", "high", "low", "close", "volume"]
|
||||
]
|
||||
df = df.drop(columns=skip_columns)
|
||||
dataframe["%-day_of_week"] = dataframe["date"].dt.dayofweek
|
||||
dataframe["%-hour_of_day"] = dataframe["date"].dt.hour
|
||||
|
||||
# Add generalized indicators here (because in live, it will call this
|
||||
# function to populate indicators during training). Notice how we ensure not to
|
||||
# add them multiple times
|
||||
if set_generalized_indicators:
|
||||
df["%-day_of_week"] = (df["date"].dt.dayofweek + 1) / 7
|
||||
df["%-hour_of_day"] = (df["date"].dt.hour + 1) / 25
|
||||
return dataframe
|
||||
|
||||
# user adds targets here by prepending them with &- (see convention below)
|
||||
# If user wishes to use multiple targets, a multioutput prediction model
|
||||
# needs to be used such as templates/CatboostPredictionMultiModel.py
|
||||
df['&s-up_or_down'] = np.where(df["close"].shift(-100) > df["close"], 'up', 'down')
|
||||
def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs):
|
||||
|
||||
return df
|
||||
dataframe['&s-up_or_down'] = np.where(dataframe["close"].shift(-100) >
|
||||
dataframe["close"], 'up', 'down')
|
||||
|
||||
return dataframe
|
||||
|
||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
|
||||
|
@@ -1,12 +1,12 @@
|
||||
import logging
|
||||
from functools import reduce
|
||||
from typing import Dict
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import talib.abstract as ta
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.strategy import DecimalParameter, IntParameter, IStrategy, merge_informative_pair
|
||||
from freqtrade.strategy import DecimalParameter, IntParameter, IStrategy
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -44,59 +44,39 @@ class freqai_test_multimodel_classifier_strat(IStrategy):
|
||||
)
|
||||
max_roi_time_long = IntParameter(0, 800, default=400, space="sell", optimize=False, load=True)
|
||||
|
||||
def populate_any_indicators(
|
||||
self, pair, df, tf, informative=None, set_generalized_indicators=False
|
||||
):
|
||||
def feature_engineering_expand_all(self, dataframe: DataFrame, period: int,
|
||||
metadata: Dict, **kwargs):
|
||||
|
||||
coin = pair.split('/')[0]
|
||||
dataframe["%-rsi-period"] = ta.RSI(dataframe, timeperiod=period)
|
||||
dataframe["%-mfi-period"] = ta.MFI(dataframe, timeperiod=period)
|
||||
dataframe["%-adx-period"] = ta.ADX(dataframe, timeperiod=period)
|
||||
|
||||
if informative is None:
|
||||
informative = self.dp.get_pair_dataframe(pair, tf)
|
||||
return dataframe
|
||||
|
||||
# first loop is automatically duplicating indicators for time periods
|
||||
for t in self.freqai_info["feature_parameters"]["indicator_periods_candles"]:
|
||||
def feature_engineering_expand_basic(self, dataframe: DataFrame, metadata: Dict, **kwargs):
|
||||
|
||||
t = int(t)
|
||||
informative[f"%-{coin}rsi-period_{t}"] = ta.RSI(informative, timeperiod=t)
|
||||
informative[f"%-{coin}mfi-period_{t}"] = ta.MFI(informative, timeperiod=t)
|
||||
informative[f"%-{coin}adx-period_{t}"] = ta.ADX(informative, window=t)
|
||||
dataframe["%-pct-change"] = dataframe["close"].pct_change()
|
||||
dataframe["%-raw_volume"] = dataframe["volume"]
|
||||
dataframe["%-raw_price"] = dataframe["close"]
|
||||
|
||||
informative[f"%-{coin}pct-change"] = informative["close"].pct_change()
|
||||
informative[f"%-{coin}raw_volume"] = informative["volume"]
|
||||
informative[f"%-{coin}raw_price"] = informative["close"]
|
||||
return dataframe
|
||||
|
||||
indicators = [col for col in informative if col.startswith("%")]
|
||||
# This loop duplicates and shifts all indicators to add a sense of recency to data
|
||||
for n in range(self.freqai_info["feature_parameters"]["include_shifted_candles"] + 1):
|
||||
if n == 0:
|
||||
continue
|
||||
informative_shift = informative[indicators].shift(n)
|
||||
informative_shift = informative_shift.add_suffix("_shift-" + str(n))
|
||||
informative = pd.concat((informative, informative_shift), axis=1)
|
||||
def feature_engineering_standard(self, dataframe: DataFrame, metadata: Dict, **kwargs):
|
||||
|
||||
df = merge_informative_pair(df, informative, self.config["timeframe"], tf, ffill=True)
|
||||
skip_columns = [
|
||||
(s + "_" + tf) for s in ["date", "open", "high", "low", "close", "volume"]
|
||||
]
|
||||
df = df.drop(columns=skip_columns)
|
||||
dataframe["%-day_of_week"] = dataframe["date"].dt.dayofweek
|
||||
dataframe["%-hour_of_day"] = dataframe["date"].dt.hour
|
||||
|
||||
# Add generalized indicators here (because in live, it will call this
|
||||
# function to populate indicators during training). Notice how we ensure not to
|
||||
# add them multiple times
|
||||
if set_generalized_indicators:
|
||||
df["%-day_of_week"] = (df["date"].dt.dayofweek + 1) / 7
|
||||
df["%-hour_of_day"] = (df["date"].dt.hour + 1) / 25
|
||||
return dataframe
|
||||
|
||||
# user adds targets here by prepending them with &- (see convention below)
|
||||
# If user wishes to use multiple targets, a multioutput prediction model
|
||||
# needs to be used such as templates/CatboostPredictionMultiModel.py
|
||||
df['&s-up_or_down'] = np.where(df["close"].shift(-50) >
|
||||
df["close"], 'up', 'down')
|
||||
def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs):
|
||||
|
||||
df['&s-up_or_down2'] = np.where(df["close"].shift(-50) >
|
||||
df["close"], 'up2', 'down2')
|
||||
dataframe['&s-up_or_down'] = np.where(dataframe["close"].shift(-50) >
|
||||
dataframe["close"], 'up', 'down')
|
||||
|
||||
return df
|
||||
dataframe['&s-up_or_down2'] = np.where(dataframe["close"].shift(-50) >
|
||||
dataframe["close"], 'up2', 'down2')
|
||||
|
||||
return dataframe
|
||||
|
||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
|
||||
|
@@ -1,11 +1,11 @@
|
||||
import logging
|
||||
from functools import reduce
|
||||
from typing import Dict
|
||||
|
||||
import pandas as pd
|
||||
import talib.abstract as ta
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.strategy import DecimalParameter, IntParameter, IStrategy, merge_informative_pair
|
||||
from freqtrade.strategy import DecimalParameter, IntParameter, IStrategy
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -43,74 +43,54 @@ class freqai_test_multimodel_strat(IStrategy):
|
||||
)
|
||||
max_roi_time_long = IntParameter(0, 800, default=400, space="sell", optimize=False, load=True)
|
||||
|
||||
def populate_any_indicators(
|
||||
self, pair, df, tf, informative=None, set_generalized_indicators=False
|
||||
):
|
||||
def feature_engineering_expand_all(self, dataframe: DataFrame, period: int,
|
||||
metadata: Dict, **kwargs):
|
||||
|
||||
coin = pair.split('/')[0]
|
||||
dataframe["%-rsi-period"] = ta.RSI(dataframe, timeperiod=period)
|
||||
dataframe["%-mfi-period"] = ta.MFI(dataframe, timeperiod=period)
|
||||
dataframe["%-adx-period"] = ta.ADX(dataframe, timeperiod=period)
|
||||
|
||||
if informative is None:
|
||||
informative = self.dp.get_pair_dataframe(pair, tf)
|
||||
return dataframe
|
||||
|
||||
# first loop is automatically duplicating indicators for time periods
|
||||
for t in self.freqai_info["feature_parameters"]["indicator_periods_candles"]:
|
||||
def feature_engineering_expand_basic(self, dataframe: DataFrame, metadata: Dict, **kwargs):
|
||||
|
||||
t = int(t)
|
||||
informative[f"%-{coin}rsi-period_{t}"] = ta.RSI(informative, timeperiod=t)
|
||||
informative[f"%-{coin}mfi-period_{t}"] = ta.MFI(informative, timeperiod=t)
|
||||
informative[f"%-{coin}adx-period_{t}"] = ta.ADX(informative, window=t)
|
||||
dataframe["%-pct-change"] = dataframe["close"].pct_change()
|
||||
dataframe["%-raw_volume"] = dataframe["volume"]
|
||||
dataframe["%-raw_price"] = dataframe["close"]
|
||||
|
||||
informative[f"%-{coin}pct-change"] = informative["close"].pct_change()
|
||||
informative[f"%-{coin}raw_volume"] = informative["volume"]
|
||||
informative[f"%-{coin}raw_price"] = informative["close"]
|
||||
return dataframe
|
||||
|
||||
indicators = [col for col in informative if col.startswith("%")]
|
||||
# This loop duplicates and shifts all indicators to add a sense of recency to data
|
||||
for n in range(self.freqai_info["feature_parameters"]["include_shifted_candles"] + 1):
|
||||
if n == 0:
|
||||
continue
|
||||
informative_shift = informative[indicators].shift(n)
|
||||
informative_shift = informative_shift.add_suffix("_shift-" + str(n))
|
||||
informative = pd.concat((informative, informative_shift), axis=1)
|
||||
def feature_engineering_standard(self, dataframe: DataFrame, metadata: Dict, **kwargs):
|
||||
|
||||
df = merge_informative_pair(df, informative, self.config["timeframe"], tf, ffill=True)
|
||||
skip_columns = [
|
||||
(s + "_" + tf) for s in ["date", "open", "high", "low", "close", "volume"]
|
||||
]
|
||||
df = df.drop(columns=skip_columns)
|
||||
dataframe["%-day_of_week"] = dataframe["date"].dt.dayofweek
|
||||
dataframe["%-hour_of_day"] = dataframe["date"].dt.hour
|
||||
|
||||
# Add generalized indicators here (because in live, it will call this
|
||||
# function to populate indicators during training). Notice how we ensure not to
|
||||
# add them multiple times
|
||||
if set_generalized_indicators:
|
||||
df["%-day_of_week"] = (df["date"].dt.dayofweek + 1) / 7
|
||||
df["%-hour_of_day"] = (df["date"].dt.hour + 1) / 25
|
||||
return dataframe
|
||||
|
||||
# user adds targets here by prepending them with &- (see convention below)
|
||||
# If user wishes to use multiple targets, a multioutput prediction model
|
||||
# needs to be used such as templates/CatboostPredictionMultiModel.py
|
||||
df["&-s_close"] = (
|
||||
df["close"]
|
||||
.shift(-self.freqai_info["feature_parameters"]["label_period_candles"])
|
||||
.rolling(self.freqai_info["feature_parameters"]["label_period_candles"])
|
||||
.mean()
|
||||
/ df["close"]
|
||||
- 1
|
||||
def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs):
|
||||
|
||||
dataframe["&-s_close"] = (
|
||||
dataframe["close"]
|
||||
.shift(-self.freqai_info["feature_parameters"]["label_period_candles"])
|
||||
.rolling(self.freqai_info["feature_parameters"]["label_period_candles"])
|
||||
.mean()
|
||||
/ dataframe["close"]
|
||||
- 1
|
||||
)
|
||||
|
||||
df["&-s_range"] = (
|
||||
df["close"]
|
||||
.shift(-self.freqai_info["feature_parameters"]["label_period_candles"])
|
||||
.rolling(self.freqai_info["feature_parameters"]["label_period_candles"])
|
||||
.max()
|
||||
-
|
||||
df["close"]
|
||||
.shift(-self.freqai_info["feature_parameters"]["label_period_candles"])
|
||||
.rolling(self.freqai_info["feature_parameters"]["label_period_candles"])
|
||||
.min()
|
||||
)
|
||||
dataframe["&-s_range"] = (
|
||||
dataframe["close"]
|
||||
.shift(-self.freqai_info["feature_parameters"]["label_period_candles"])
|
||||
.rolling(self.freqai_info["feature_parameters"]["label_period_candles"])
|
||||
.max()
|
||||
-
|
||||
dataframe["close"]
|
||||
.shift(-self.freqai_info["feature_parameters"]["label_period_candles"])
|
||||
.rolling(self.freqai_info["feature_parameters"]["label_period_candles"])
|
||||
.min()
|
||||
)
|
||||
|
||||
return df
|
||||
return dataframe
|
||||
|
||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
|
||||
|
@@ -1,11 +1,11 @@
|
||||
import logging
|
||||
from functools import reduce
|
||||
from typing import Dict
|
||||
|
||||
import pandas as pd
|
||||
import talib.abstract as ta
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.strategy import DecimalParameter, IntParameter, IStrategy, merge_informative_pair
|
||||
from freqtrade.strategy import DecimalParameter, IntParameter, IStrategy
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -43,62 +43,42 @@ class freqai_test_strat(IStrategy):
|
||||
)
|
||||
max_roi_time_long = IntParameter(0, 800, default=400, space="sell", optimize=False, load=True)
|
||||
|
||||
def populate_any_indicators(
|
||||
self, pair, df, tf, informative=None, set_generalized_indicators=False
|
||||
):
|
||||
def feature_engineering_expand_all(self, dataframe: DataFrame, period: int,
|
||||
metadata: Dict, **kwargs):
|
||||
|
||||
coin = pair.split('/')[0]
|
||||
dataframe["%-rsi-period"] = ta.RSI(dataframe, timeperiod=period)
|
||||
dataframe["%-mfi-period"] = ta.MFI(dataframe, timeperiod=period)
|
||||
dataframe["%-adx-period"] = ta.ADX(dataframe, timeperiod=period)
|
||||
|
||||
if informative is None:
|
||||
informative = self.dp.get_pair_dataframe(pair, tf)
|
||||
return dataframe
|
||||
|
||||
# first loop is automatically duplicating indicators for time periods
|
||||
for t in self.freqai_info["feature_parameters"]["indicator_periods_candles"]:
|
||||
def feature_engineering_expand_basic(self, dataframe: DataFrame, metadata: Dict, **kwargs):
|
||||
|
||||
t = int(t)
|
||||
informative[f"%-{coin}rsi-period_{t}"] = ta.RSI(informative, timeperiod=t)
|
||||
informative[f"%-{coin}mfi-period_{t}"] = ta.MFI(informative, timeperiod=t)
|
||||
informative[f"%-{coin}adx-period_{t}"] = ta.ADX(informative, window=t)
|
||||
dataframe["%-pct-change"] = dataframe["close"].pct_change()
|
||||
dataframe["%-raw_volume"] = dataframe["volume"]
|
||||
dataframe["%-raw_price"] = dataframe["close"]
|
||||
|
||||
informative[f"%-{coin}pct-change"] = informative["close"].pct_change()
|
||||
informative[f"%-{coin}raw_volume"] = informative["volume"]
|
||||
informative[f"%-{coin}raw_price"] = informative["close"]
|
||||
return dataframe
|
||||
|
||||
indicators = [col for col in informative if col.startswith("%")]
|
||||
# This loop duplicates and shifts all indicators to add a sense of recency to data
|
||||
for n in range(self.freqai_info["feature_parameters"]["include_shifted_candles"] + 1):
|
||||
if n == 0:
|
||||
continue
|
||||
informative_shift = informative[indicators].shift(n)
|
||||
informative_shift = informative_shift.add_suffix("_shift-" + str(n))
|
||||
informative = pd.concat((informative, informative_shift), axis=1)
|
||||
def feature_engineering_standard(self, dataframe: DataFrame, metadata: Dict, **kwargs):
|
||||
|
||||
df = merge_informative_pair(df, informative, self.config["timeframe"], tf, ffill=True)
|
||||
skip_columns = [
|
||||
(s + "_" + tf) for s in ["date", "open", "high", "low", "close", "volume"]
|
||||
]
|
||||
df = df.drop(columns=skip_columns)
|
||||
dataframe["%-day_of_week"] = dataframe["date"].dt.dayofweek
|
||||
dataframe["%-hour_of_day"] = dataframe["date"].dt.hour
|
||||
|
||||
# Add generalized indicators here (because in live, it will call this
|
||||
# function to populate indicators during training). Notice how we ensure not to
|
||||
# add them multiple times
|
||||
if set_generalized_indicators:
|
||||
df["%-day_of_week"] = (df["date"].dt.dayofweek + 1) / 7
|
||||
df["%-hour_of_day"] = (df["date"].dt.hour + 1) / 25
|
||||
return dataframe
|
||||
|
||||
# user adds targets here by prepending them with &- (see convention below)
|
||||
# If user wishes to use multiple targets, a multioutput prediction model
|
||||
# needs to be used such as templates/CatboostPredictionMultiModel.py
|
||||
df["&-s_close"] = (
|
||||
df["close"]
|
||||
.shift(-self.freqai_info["feature_parameters"]["label_period_candles"])
|
||||
.rolling(self.freqai_info["feature_parameters"]["label_period_candles"])
|
||||
.mean()
|
||||
/ df["close"]
|
||||
- 1
|
||||
def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs):
|
||||
|
||||
dataframe["&-s_close"] = (
|
||||
dataframe["close"]
|
||||
.shift(-self.freqai_info["feature_parameters"]["label_period_candles"])
|
||||
.rolling(self.freqai_info["feature_parameters"]["label_period_candles"])
|
||||
.mean()
|
||||
/ dataframe["close"]
|
||||
- 1
|
||||
)
|
||||
|
||||
return df
|
||||
return dataframe
|
||||
|
||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
|
||||
|
@@ -34,6 +34,11 @@ class HyperoptableStrategy(StrategyTestV3):
|
||||
protection_enabled = BooleanParameter(default=True)
|
||||
protection_cooldown_lookback = IntParameter([0, 50], default=30)
|
||||
|
||||
# Invalid plot config ...
|
||||
plot_config = {
|
||||
"main_plot": {},
|
||||
}
|
||||
|
||||
@property
|
||||
def protections(self):
|
||||
prot = []
|
||||
|
@@ -30,6 +30,9 @@ class StrategyTestV3(IStrategy):
|
||||
"0": 0.04
|
||||
}
|
||||
|
||||
# Optimal max_open_trades for the strategy
|
||||
max_open_trades = -1
|
||||
|
||||
# Optimal stoploss designed for the strategy
|
||||
stoploss = -0.10
|
||||
|
||||
@@ -194,7 +197,7 @@ class StrategyTestV3(IStrategy):
|
||||
|
||||
if current_profit < -0.0075:
|
||||
orders = trade.select_filled_orders(trade.entry_side)
|
||||
return round(orders[0].cost, 0)
|
||||
return round(orders[0].safe_cost, 0)
|
||||
|
||||
return None
|
||||
|
||||
|
@@ -214,12 +214,12 @@ def test_ignore_expired_candle(default_conf):
|
||||
|
||||
current_time = latest_date + timedelta(seconds=30 + 300)
|
||||
|
||||
assert not strategy.ignore_expired_candle(
|
||||
assert strategy.ignore_expired_candle(
|
||||
latest_date=latest_date,
|
||||
current_time=current_time,
|
||||
timeframe_seconds=300,
|
||||
enter=True
|
||||
) is True
|
||||
) is not True
|
||||
|
||||
|
||||
def test_assert_df_raise(mocker, caplog, ohlcv_history):
|
||||
@@ -291,18 +291,6 @@ def test_advise_all_indicators(default_conf, testdatadir) -> None:
|
||||
assert len(processed['UNITTEST/BTC']) == 103
|
||||
|
||||
|
||||
def test_populate_any_indicators(default_conf, testdatadir) -> None:
|
||||
strategy = StrategyResolver.load_strategy(default_conf)
|
||||
|
||||
timerange = TimeRange.parse_timerange('1510694220-1510700340')
|
||||
data = load_data(testdatadir, '1m', ['UNITTEST/BTC'], timerange=timerange,
|
||||
fill_up_missing=True)
|
||||
processed = strategy.populate_any_indicators('UNITTEST/BTC', data, '5m')
|
||||
assert processed == data
|
||||
assert id(processed) == id(data)
|
||||
assert len(processed['UNITTEST/BTC']) == 103
|
||||
|
||||
|
||||
def test_freqai_not_initialized(default_conf) -> None:
|
||||
strategy = StrategyResolver.load_strategy(default_conf)
|
||||
strategy.ft_bot_start()
|
||||
@@ -452,8 +440,8 @@ def test_min_roi_reached3(default_conf, fee) -> None:
|
||||
(0.05, 0.9, ExitType.NONE, None, False, True, 0.09, 0.9, ExitType.NONE,
|
||||
lambda **kwargs: None),
|
||||
])
|
||||
def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, liq, trailing, custom,
|
||||
profit2, adjusted2, expected2, custom_stop) -> None:
|
||||
def test_ft_stoploss_reached(default_conf, fee, profit, adjusted, expected, liq, trailing, custom,
|
||||
profit2, adjusted2, expected2, custom_stop) -> None:
|
||||
|
||||
strategy = StrategyResolver.load_strategy(default_conf)
|
||||
trade = Trade(
|
||||
@@ -477,9 +465,9 @@ def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, liq, t
|
||||
|
||||
now = arrow.utcnow().datetime
|
||||
current_rate = trade.open_rate * (1 + profit)
|
||||
sl_flag = strategy.stop_loss_reached(current_rate=current_rate, trade=trade,
|
||||
current_time=now, current_profit=profit,
|
||||
force_stoploss=0, high=None)
|
||||
sl_flag = strategy.ft_stoploss_reached(current_rate=current_rate, trade=trade,
|
||||
current_time=now, current_profit=profit,
|
||||
force_stoploss=0, high=None)
|
||||
assert isinstance(sl_flag, ExitCheckTuple)
|
||||
assert sl_flag.exit_type == expected
|
||||
if expected == ExitType.NONE:
|
||||
@@ -489,9 +477,9 @@ def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, liq, t
|
||||
assert round(trade.stop_loss, 2) == adjusted
|
||||
current_rate2 = trade.open_rate * (1 + profit2)
|
||||
|
||||
sl_flag = strategy.stop_loss_reached(current_rate=current_rate2, trade=trade,
|
||||
current_time=now, current_profit=profit2,
|
||||
force_stoploss=0, high=None)
|
||||
sl_flag = strategy.ft_stoploss_reached(current_rate=current_rate2, trade=trade,
|
||||
current_time=now, current_profit=profit2,
|
||||
force_stoploss=0, high=None)
|
||||
assert sl_flag.exit_type == expected2
|
||||
if expected2 == ExitType.NONE:
|
||||
assert sl_flag.exit_flag is False
|
||||
@@ -579,7 +567,7 @@ def test_should_sell(default_conf, fee) -> None:
|
||||
assert res == [ExitCheckTuple(exit_type=ExitType.ROI)]
|
||||
|
||||
strategy.min_roi_reached = MagicMock(return_value=True)
|
||||
strategy.stop_loss_reached = MagicMock(
|
||||
strategy.ft_stoploss_reached = MagicMock(
|
||||
return_value=ExitCheckTuple(exit_type=ExitType.STOP_LOSS))
|
||||
|
||||
res = strategy.should_exit(trade, 1, now,
|
||||
@@ -603,7 +591,7 @@ def test_should_sell(default_conf, fee) -> None:
|
||||
ExitCheckTuple(exit_type=ExitType.ROI),
|
||||
]
|
||||
|
||||
strategy.stop_loss_reached = MagicMock(
|
||||
strategy.ft_stoploss_reached = MagicMock(
|
||||
return_value=ExitCheckTuple(exit_type=ExitType.TRAILING_STOP_LOSS))
|
||||
# Regular exit signal
|
||||
res = strategy.should_exit(trade, 1, now,
|
||||
|
@@ -119,53 +119,88 @@ def test_merge_informative_pair_suffix_append_timeframe():
|
||||
merge_informative_pair(data, informative, '15m', '1h', suffix="suf")
|
||||
|
||||
|
||||
def test_stoploss_from_open():
|
||||
@pytest.mark.parametrize("side,profitrange", [
|
||||
# profit range for long is [-1, inf] while for shorts is [-inf, 1]
|
||||
("long", [-0.99, 2, 30]),
|
||||
("short", [-2.0, 0.99, 30]),
|
||||
])
|
||||
def test_stoploss_from_open(side, profitrange):
|
||||
open_price_ranges = [
|
||||
[0.01, 1.00, 30],
|
||||
[1, 100, 30],
|
||||
[100, 10000, 30],
|
||||
]
|
||||
# profit range for long is [-1, inf] while for shorts is [-inf, 1]
|
||||
current_profit_range_dict = {'long': [-0.99, 2, 30], 'short': [-2.0, 0.99, 30]}
|
||||
desired_stop_range = [-0.50, 0.50, 30]
|
||||
|
||||
for side, current_profit_range in current_profit_range_dict.items():
|
||||
for open_range in open_price_ranges:
|
||||
for open_price in np.linspace(*open_range):
|
||||
for desired_stop in np.linspace(*desired_stop_range):
|
||||
for open_range in open_price_ranges:
|
||||
for open_price in np.linspace(*open_range):
|
||||
for desired_stop in np.linspace(-0.50, 0.50, 30):
|
||||
|
||||
if side == 'long':
|
||||
# -1 is not a valid current_profit, should return 1
|
||||
assert stoploss_from_open(desired_stop, -1) == 1
|
||||
else:
|
||||
# 1 is not a valid current_profit for shorts, should return 1
|
||||
assert stoploss_from_open(desired_stop, 1, True) == 1
|
||||
|
||||
for current_profit in np.linspace(*profitrange):
|
||||
if side == 'long':
|
||||
# -1 is not a valid current_profit, should return 1
|
||||
assert stoploss_from_open(desired_stop, -1) == 1
|
||||
current_price = open_price * (1 + current_profit)
|
||||
expected_stop_price = open_price * (1 + desired_stop)
|
||||
stoploss = stoploss_from_open(desired_stop, current_profit)
|
||||
stop_price = current_price * (1 - stoploss)
|
||||
else:
|
||||
# 1 is not a valid current_profit for shorts, should return 1
|
||||
assert stoploss_from_open(desired_stop, 1, True) == 1
|
||||
current_price = open_price * (1 - current_profit)
|
||||
expected_stop_price = open_price * (1 - desired_stop)
|
||||
stoploss = stoploss_from_open(desired_stop, current_profit, True)
|
||||
stop_price = current_price * (1 + stoploss)
|
||||
|
||||
for current_profit in np.linspace(*current_profit_range):
|
||||
if side == 'long':
|
||||
current_price = open_price * (1 + current_profit)
|
||||
expected_stop_price = open_price * (1 + desired_stop)
|
||||
stoploss = stoploss_from_open(desired_stop, current_profit)
|
||||
stop_price = current_price * (1 - stoploss)
|
||||
else:
|
||||
current_price = open_price * (1 - current_profit)
|
||||
expected_stop_price = open_price * (1 - desired_stop)
|
||||
stoploss = stoploss_from_open(desired_stop, current_profit, True)
|
||||
stop_price = current_price * (1 + stoploss)
|
||||
assert stoploss >= 0
|
||||
# Technically the formula can yield values greater than 1 for shorts
|
||||
# eventhough it doesn't make sense because the position would be liquidated
|
||||
if side == 'long':
|
||||
assert stoploss <= 1
|
||||
|
||||
assert stoploss >= 0
|
||||
# Technically the formula can yield values greater than 1 for shorts
|
||||
# eventhough it doesn't make sense because the position would be liquidated
|
||||
if side == 'long':
|
||||
assert stoploss <= 1
|
||||
# there is no correct answer if the expected stop price is above
|
||||
# the current price
|
||||
if ((side == 'long' and expected_stop_price > current_price)
|
||||
or (side == 'short' and expected_stop_price < current_price)):
|
||||
assert stoploss == 0
|
||||
else:
|
||||
assert pytest.approx(stop_price) == expected_stop_price
|
||||
|
||||
# there is no correct answer if the expected stop price is above
|
||||
# the current price
|
||||
if ((side == 'long' and expected_stop_price > current_price)
|
||||
or (side == 'short' and expected_stop_price < current_price)):
|
||||
assert stoploss == 0
|
||||
else:
|
||||
assert pytest.approx(stop_price) == expected_stop_price
|
||||
|
||||
@pytest.mark.parametrize("side,rel_stop,curr_profit,leverage,expected", [
|
||||
# profit range for long is [-1, inf] while for shorts is [-inf, 1]
|
||||
("long", 0, -1, 1, 1),
|
||||
("long", 0, 0.1, 1, 0.09090909),
|
||||
("long", -0.1, 0.1, 1, 0.18181818),
|
||||
("long", 0.1, 0.2, 1, 0.08333333),
|
||||
("long", 0.1, 0.5, 1, 0.266666666),
|
||||
("long", 0.1, 5, 1, 0.816666666), # 500% profit, set stoploss to 10% above open price
|
||||
("long", 0, 5, 10, 3.3333333), # 500% profit, set stoploss break even
|
||||
("long", 0.1, 5, 10, 3.26666666), # 500% profit, set stoploss to 10% above open price
|
||||
("long", -0.1, 5, 10, 3.3999999), # 500% profit, set stoploss to 10% belowopen price
|
||||
|
||||
("short", 0, 0.1, 1, 0.1111111),
|
||||
("short", -0.1, 0.1, 1, 0.2222222),
|
||||
("short", 0.1, 0.2, 1, 0.125),
|
||||
("short", 0.1, 1, 1, 1),
|
||||
("short", -0.01, 5, 10, 10.01999999), # 500% profit at 10x
|
||||
])
|
||||
def test_stoploss_from_open_leverage(side, rel_stop, curr_profit, leverage, expected):
|
||||
|
||||
stoploss = stoploss_from_open(rel_stop, curr_profit, side == 'short', leverage)
|
||||
assert pytest.approx(stoploss) == expected
|
||||
open_rate = 100
|
||||
if stoploss != 1:
|
||||
if side == 'long':
|
||||
current_rate = open_rate * (1 + curr_profit / leverage)
|
||||
stop = current_rate * (1 - stoploss / leverage)
|
||||
assert pytest.approx(stop) == open_rate * (1 + rel_stop / leverage)
|
||||
else:
|
||||
current_rate = open_rate * (1 - curr_profit / leverage)
|
||||
stop = current_rate * (1 + stoploss / leverage)
|
||||
assert pytest.approx(stop) == open_rate * (1 - rel_stop / leverage)
|
||||
|
||||
|
||||
def test_stoploss_from_absolute():
|
||||
|
@@ -6,6 +6,7 @@ from pathlib import Path
|
||||
import pytest
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.configuration import Configuration
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.resolvers import StrategyResolver
|
||||
from freqtrade.strategy.interface import IStrategy
|
||||
@@ -175,6 +176,18 @@ def test_strategy_override_stoploss(caplog, default_conf):
|
||||
assert log_has("Override strategy 'stoploss' with value in config file: -0.5.", caplog)
|
||||
|
||||
|
||||
def test_strategy_override_max_open_trades(caplog, default_conf):
|
||||
caplog.set_level(logging.INFO)
|
||||
default_conf.update({
|
||||
'strategy': CURRENT_TEST_STRATEGY,
|
||||
'max_open_trades': 7
|
||||
})
|
||||
strategy = StrategyResolver.load_strategy(default_conf)
|
||||
|
||||
assert strategy.max_open_trades == 7
|
||||
assert log_has("Override strategy 'max_open_trades' with value in config file: 7.", caplog)
|
||||
|
||||
|
||||
def test_strategy_override_trailing_stop(caplog, default_conf):
|
||||
caplog.set_level(logging.INFO)
|
||||
default_conf.update({
|
||||
@@ -349,6 +362,38 @@ def test_strategy_override_use_exit_profit_only(caplog, default_conf):
|
||||
assert log_has("Override strategy 'exit_profit_only' with value in config file: True.", caplog)
|
||||
|
||||
|
||||
def test_strategy_max_open_trades_infinity_from_strategy(caplog, default_conf):
|
||||
caplog.set_level(logging.INFO)
|
||||
default_conf.update({
|
||||
'strategy': CURRENT_TEST_STRATEGY,
|
||||
})
|
||||
del default_conf['max_open_trades']
|
||||
|
||||
strategy = StrategyResolver.load_strategy(default_conf)
|
||||
|
||||
# this test assumes -1 set to 'max_open_trades' in CURRENT_TEST_STRATEGY
|
||||
assert strategy.max_open_trades == float('inf')
|
||||
assert default_conf['max_open_trades'] == float('inf')
|
||||
|
||||
|
||||
def test_strategy_max_open_trades_infinity_from_config(caplog, default_conf, mocker):
|
||||
caplog.set_level(logging.INFO)
|
||||
default_conf.update({
|
||||
'strategy': CURRENT_TEST_STRATEGY,
|
||||
'max_open_trades': -1,
|
||||
'exchange': 'binance'
|
||||
})
|
||||
|
||||
configuration = Configuration(args=default_conf)
|
||||
parsed_config = configuration.get_config()
|
||||
|
||||
assert parsed_config['max_open_trades'] == float('inf')
|
||||
|
||||
strategy = StrategyResolver.load_strategy(parsed_config)
|
||||
|
||||
assert strategy.max_open_trades == float('inf')
|
||||
|
||||
|
||||
@ pytest.mark.filterwarnings("ignore:deprecated")
|
||||
def test_missing_implements(default_conf, caplog):
|
||||
|
||||
@@ -438,3 +483,19 @@ def test_strategy_interface_versioning(dataframe_1m, default_conf):
|
||||
assert isinstance(exitdf, DataFrame)
|
||||
assert 'sell' not in exitdf
|
||||
assert 'exit_long' in exitdf
|
||||
|
||||
|
||||
def test_strategy_ft_load_params_from_file(mocker, default_conf):
|
||||
default_conf.update({'strategy': 'StrategyTestV2'})
|
||||
del default_conf['max_open_trades']
|
||||
mocker.patch('freqtrade.strategy.hyper.HyperStrategyMixin.load_params_from_file',
|
||||
return_value={
|
||||
'params': {
|
||||
'max_open_trades': {
|
||||
'max_open_trades': -1
|
||||
}
|
||||
}
|
||||
})
|
||||
strategy = StrategyResolver.load_strategy(default_conf)
|
||||
assert strategy.max_open_trades == float('inf')
|
||||
assert strategy.config['max_open_trades'] == float('inf')
|
||||
|
58
tests/test_binance_mig.py
Normal file
58
tests/test_binance_mig.py
Normal file
@@ -0,0 +1,58 @@
|
||||
|
||||
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.util.binance_mig import migrate_binance_futures_data, migrate_binance_futures_names
|
||||
from tests.conftest import create_mock_trades_usdt, log_has
|
||||
|
||||
|
||||
def test_binance_mig_data_conversion(default_conf_usdt, tmpdir, testdatadir):
|
||||
|
||||
# call doing nothing (spot mode)
|
||||
migrate_binance_futures_data(default_conf_usdt)
|
||||
default_conf_usdt['trading_mode'] = 'futures'
|
||||
pair_old = 'XRP_USDT'
|
||||
pair_unified = 'XRP_USDT_USDT'
|
||||
futures_src = testdatadir / 'futures'
|
||||
futures_dst = tmpdir / 'futures'
|
||||
futures_dst.mkdir()
|
||||
files = [
|
||||
'-1h-mark.json',
|
||||
'-1h-futures.json',
|
||||
'-8h-funding_rate.json',
|
||||
'-8h-mark.json',
|
||||
]
|
||||
|
||||
# Copy files to tmpdir and rename to old naming
|
||||
for file in files:
|
||||
fn_after = futures_dst / f'{pair_old}{file}'
|
||||
shutil.copy(futures_src / f'{pair_unified}{file}', fn_after)
|
||||
|
||||
default_conf_usdt['datadir'] = Path(tmpdir)
|
||||
# Migrate files to unified namings
|
||||
migrate_binance_futures_data(default_conf_usdt)
|
||||
|
||||
for file in files:
|
||||
fn_after = futures_dst / f'{pair_unified}{file}'
|
||||
assert fn_after.exists()
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_persistence")
|
||||
def test_binance_mig_db_conversion(default_conf_usdt, fee, caplog):
|
||||
# Does nothing in spot mode
|
||||
migrate_binance_futures_names(default_conf_usdt)
|
||||
|
||||
create_mock_trades_usdt(fee, None)
|
||||
|
||||
for t in Trade.get_trades():
|
||||
t.trading_mode = 'FUTURES'
|
||||
t.exchange = 'binance'
|
||||
Trade.commit()
|
||||
|
||||
default_conf_usdt['trading_mode'] = 'futures'
|
||||
migrate_binance_futures_names(default_conf_usdt)
|
||||
assert log_has('Migrating binance futures pairs in database.', caplog)
|
@@ -58,7 +58,8 @@ def test_load_config_incorrect_stake_amount(default_conf) -> None:
|
||||
|
||||
def test_load_config_file(default_conf, mocker, caplog) -> None:
|
||||
del default_conf['user_data_dir']
|
||||
file_mock = mocker.patch('freqtrade.configuration.load_config.open', mocker.mock_open(
|
||||
default_conf['datadir'] = str(default_conf['datadir'])
|
||||
file_mock = mocker.patch('freqtrade.configuration.load_config.Path.open', mocker.mock_open(
|
||||
read_data=json.dumps(default_conf)
|
||||
))
|
||||
|
||||
@@ -69,9 +70,11 @@ def test_load_config_file(default_conf, mocker, caplog) -> None:
|
||||
|
||||
def test_load_config_file_error(default_conf, mocker, caplog) -> None:
|
||||
del default_conf['user_data_dir']
|
||||
default_conf['datadir'] = str(default_conf['datadir'])
|
||||
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('freqtrade.configuration.load_config.Path.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.*"):
|
||||
@@ -80,6 +83,7 @@ def test_load_config_file_error(default_conf, mocker, caplog) -> None:
|
||||
|
||||
def test_load_config_file_error_range(default_conf, mocker, caplog) -> None:
|
||||
del default_conf['user_data_dir']
|
||||
default_conf['datadir'] = str(default_conf['datadir'])
|
||||
filedata = json.dumps(default_conf).replace(
|
||||
'"stake_amount": 0.001,', '"stake_amount": .001,')
|
||||
mocker.patch.object(Path, "read_text", MagicMock(return_value=filedata))
|
||||
@@ -238,6 +242,7 @@ def test_print_config(default_conf, mocker, caplog) -> None:
|
||||
conf1 = deepcopy(default_conf)
|
||||
# Delete non-json elements from default_conf
|
||||
del conf1['user_data_dir']
|
||||
conf1['datadir'] = str(conf1['datadir'])
|
||||
config_files = [conf1]
|
||||
|
||||
configsmock = MagicMock(side_effect=config_files)
|
||||
@@ -268,7 +273,7 @@ def test_load_config_max_open_trades_minus_one(default_conf, mocker, caplog) ->
|
||||
|
||||
def test_load_config_file_exception(mocker) -> None:
|
||||
mocker.patch(
|
||||
'freqtrade.configuration.configuration.open',
|
||||
'freqtrade.configuration.configuration.Path.open',
|
||||
MagicMock(side_effect=FileNotFoundError('File not found'))
|
||||
)
|
||||
|
||||
@@ -697,15 +702,16 @@ def test_set_loggers_journald(mocker):
|
||||
'logfile': 'journald',
|
||||
}
|
||||
|
||||
setup_logging_pre()
|
||||
setup_logging(config)
|
||||
assert len(logger.handlers) == 2
|
||||
assert len(logger.handlers) == 3
|
||||
assert [x for x in logger.handlers if type(x).__name__ == "JournaldLogHandler"]
|
||||
assert [x for x in logger.handlers if type(x) == logging.StreamHandler]
|
||||
# reset handlers to not break pytest
|
||||
logger.handlers = orig_handlers
|
||||
|
||||
|
||||
def test_set_loggers_journald_importerror(mocker, import_fails):
|
||||
def test_set_loggers_journald_importerror(import_fails):
|
||||
logger = logging.getLogger()
|
||||
orig_handlers = logger.handlers
|
||||
logger.handlers = []
|
||||
@@ -714,7 +720,7 @@ def test_set_loggers_journald_importerror(mocker, import_fails):
|
||||
'logfile': 'journald',
|
||||
}
|
||||
with pytest.raises(OperationalException,
|
||||
match=r'You need the systemd python package.*'):
|
||||
match=r'You need the cysystemd python package.*'):
|
||||
setup_logging(config)
|
||||
logger.handlers = orig_handlers
|
||||
|
||||
@@ -1046,8 +1052,13 @@ def test__validate_freqai_include_timeframes(default_conf, caplog) -> None:
|
||||
# Validation pass
|
||||
conf.update({'timeframe': '1m'})
|
||||
validate_config_consistency(conf)
|
||||
conf.update({'analyze_per_epoch': True})
|
||||
|
||||
# Ensure base timeframe is in include_timeframes
|
||||
conf['freqai']['feature_parameters']['include_timeframes'] = ["5m", "15m"]
|
||||
validate_config_consistency(conf)
|
||||
assert conf['freqai']['feature_parameters']['include_timeframes'] == ["1m", "5m", "15m"]
|
||||
|
||||
conf.update({'analyze_per_epoch': True})
|
||||
with pytest.raises(OperationalException,
|
||||
match=r"Using analyze-per-epoch .* not supported with a FreqAI strategy."):
|
||||
validate_config_consistency(conf)
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,13 @@
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import select
|
||||
|
||||
from freqtrade.enums import ExitCheckTuple, ExitType, TradingMode
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.persistence.models import Order
|
||||
from freqtrade.rpc.rpc import RPC
|
||||
from tests.conftest import get_patched_freqtradebot, log_has_re, patch_get_signal
|
||||
from tests.conftest import EXMS, get_patched_freqtradebot, log_has_re, patch_get_signal
|
||||
|
||||
|
||||
def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee,
|
||||
@@ -56,9 +57,9 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee,
|
||||
[ExitCheckTuple(exit_type=ExitType.EXIT_SIGNAL)]]
|
||||
)
|
||||
cancel_order_mock = MagicMock()
|
||||
mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
create_stoploss=stoploss,
|
||||
fetch_ticker=ticker,
|
||||
get_fee=fee,
|
||||
amount_to_precision=lambda s, x, y: y,
|
||||
@@ -91,7 +92,7 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee,
|
||||
assert freqtrade.strategy.confirm_trade_exit.call_count == 0
|
||||
wallets_mock.reset_mock()
|
||||
|
||||
trades = Trade.query.all()
|
||||
trades = Trade.session.scalars(select(Trade)).all()
|
||||
# Make sure stoploss-order is open and trade is bought (since we mock update_trade_state)
|
||||
for trade in trades:
|
||||
stoploss_order_closed['id'] = '3'
|
||||
@@ -147,7 +148,7 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, mocker, balance_rati
|
||||
default_conf['telegram']['enabled'] = True
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
fetch_ticker=ticker,
|
||||
get_fee=fee,
|
||||
amount_to_precision=lambda s, x, y: y,
|
||||
@@ -179,13 +180,13 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, mocker, balance_rati
|
||||
n = freqtrade.enter_positions()
|
||||
assert n == 4
|
||||
|
||||
trades = Trade.query.all()
|
||||
trades = Trade.session.scalars(select(Trade)).all()
|
||||
assert len(trades) == 4
|
||||
assert freqtrade.wallets.get_trade_stake_amount('XRP/BTC') == result1
|
||||
|
||||
rpc._rpc_force_entry('TKN/BTC', None)
|
||||
|
||||
trades = Trade.query.all()
|
||||
trades = Trade.session.scalars(select(Trade)).all()
|
||||
assert len(trades) == 5
|
||||
|
||||
for trade in trades:
|
||||
@@ -217,7 +218,7 @@ def test_dca_buying(default_conf_usdt, ticker_usdt, fee, mocker) -> None:
|
||||
|
||||
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
fetch_ticker=ticker_usdt,
|
||||
get_fee=fee,
|
||||
)
|
||||
@@ -239,7 +240,7 @@ def test_dca_buying(default_conf_usdt, ticker_usdt, fee, mocker) -> None:
|
||||
# Reduce bid amount
|
||||
ticker_usdt_modif = ticker_usdt.return_value
|
||||
ticker_usdt_modif['bid'] = ticker_usdt_modif['bid'] * 0.995
|
||||
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', return_value=ticker_usdt_modif)
|
||||
mocker.patch(f'{EXMS}.fetch_ticker', return_value=ticker_usdt_modif)
|
||||
|
||||
# additional buy order
|
||||
freqtrade.process()
|
||||
@@ -286,7 +287,7 @@ def test_dca_short(default_conf_usdt, ticker_usdt, fee, mocker) -> None:
|
||||
|
||||
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
fetch_ticker=ticker_usdt,
|
||||
get_fee=fee,
|
||||
amount_to_precision=lambda s, x, y: round(y, 4),
|
||||
@@ -311,7 +312,7 @@ def test_dca_short(default_conf_usdt, ticker_usdt, fee, mocker) -> None:
|
||||
# Reduce bid amount
|
||||
ticker_usdt_modif = ticker_usdt.return_value
|
||||
ticker_usdt_modif['ask'] = ticker_usdt_modif['ask'] * 1.004
|
||||
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', return_value=ticker_usdt_modif)
|
||||
mocker.patch(f'{EXMS}.fetch_ticker', return_value=ticker_usdt_modif)
|
||||
|
||||
# additional buy order
|
||||
freqtrade.process()
|
||||
@@ -361,16 +362,16 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker)
|
||||
|
||||
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
fetch_ticker=ticker_usdt,
|
||||
get_fee=fee,
|
||||
amount_to_precision=lambda s, x, y: y,
|
||||
price_to_precision=lambda s, x, y: y,
|
||||
)
|
||||
mocker.patch('freqtrade.exchange.Exchange._is_dry_limit_order_filled', return_value=False)
|
||||
mocker.patch("freqtrade.exchange.Exchange.get_max_leverage", return_value=10)
|
||||
mocker.patch("freqtrade.exchange.Exchange.get_funding_fees", return_value=0)
|
||||
mocker.patch("freqtrade.exchange.Exchange.get_maintenance_ratio_and_amt", return_value=(0, 0))
|
||||
mocker.patch(f'{EXMS}._dry_is_price_crossed', return_value=False)
|
||||
mocker.patch(f"{EXMS}.get_max_leverage", return_value=10)
|
||||
mocker.patch(f"{EXMS}.get_funding_fees", return_value=0)
|
||||
mocker.patch(f"{EXMS}.get_maintenance_ratio_and_amt", return_value=(0, 0))
|
||||
|
||||
patch_get_signal(freqtrade)
|
||||
freqtrade.strategy.custom_entry_price = lambda **kwargs: ticker_usdt['ask'] * 0.96
|
||||
@@ -413,7 +414,7 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker)
|
||||
assert trade.initial_stop_loss_pct is None
|
||||
|
||||
# Fill order
|
||||
mocker.patch('freqtrade.exchange.Exchange._is_dry_limit_order_filled', return_value=True)
|
||||
mocker.patch(f'{EXMS}._dry_is_price_crossed', return_value=True)
|
||||
freqtrade.process()
|
||||
trade = Trade.get_trades().first()
|
||||
assert len(trade.orders) == 2
|
||||
@@ -428,7 +429,7 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker)
|
||||
|
||||
# 2nd order - not filling
|
||||
freqtrade.strategy.adjust_trade_position = MagicMock(return_value=120)
|
||||
mocker.patch('freqtrade.exchange.Exchange._is_dry_limit_order_filled', return_value=False)
|
||||
mocker.patch(f'{EXMS}._dry_is_price_crossed', return_value=False)
|
||||
|
||||
freqtrade.process()
|
||||
trade = Trade.get_trades().first()
|
||||
@@ -452,7 +453,7 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker)
|
||||
|
||||
# Fill DCA order
|
||||
freqtrade.strategy.adjust_trade_position = MagicMock(return_value=None)
|
||||
mocker.patch('freqtrade.exchange.Exchange._is_dry_limit_order_filled', return_value=True)
|
||||
mocker.patch(f'{EXMS}._dry_is_price_crossed', return_value=True)
|
||||
freqtrade.strategy.adjust_entry_price = MagicMock(side_effect=ValueError)
|
||||
|
||||
freqtrade.process()
|
||||
@@ -477,14 +478,14 @@ def test_dca_exiting(default_conf_usdt, ticker_usdt, fee, mocker, caplog, levera
|
||||
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||
freqtrade.trading_mode = TradingMode.FUTURES
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
fetch_ticker=ticker_usdt,
|
||||
get_fee=fee,
|
||||
amount_to_precision=lambda s, x, y: y,
|
||||
price_to_precision=lambda s, x, y: y,
|
||||
get_min_pair_stake_amount=MagicMock(return_value=10),
|
||||
)
|
||||
mocker.patch("freqtrade.exchange.Exchange.get_max_leverage", return_value=10)
|
||||
mocker.patch(f"{EXMS}.get_max_leverage", return_value=10)
|
||||
|
||||
patch_get_signal(freqtrade)
|
||||
freqtrade.strategy.leverage = MagicMock(return_value=leverage)
|
||||
@@ -532,8 +533,7 @@ def test_dca_exiting(default_conf_usdt, ticker_usdt, fee, mocker, caplog, levera
|
||||
assert trade.is_open
|
||||
|
||||
# use amount that would trunc to 0.0 once selling
|
||||
mocker.patch("freqtrade.exchange.Exchange.amount_to_contract_precision",
|
||||
lambda s, p, v: round(v, 1))
|
||||
mocker.patch(f"{EXMS}.amount_to_contract_precision", lambda s, p, v: round(v, 1))
|
||||
freqtrade.strategy.adjust_trade_position = MagicMock(return_value=-0.01)
|
||||
freqtrade.process()
|
||||
trade = Trade.get_trades().first()
|
||||
|
@@ -5,6 +5,7 @@ from copy import deepcopy
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pandas as pd
|
||||
import pytest
|
||||
|
||||
from freqtrade.misc import (dataframe_to_json, decimals_per_coin, deep_merge_dicts, file_dump_json,
|
||||
@@ -45,7 +46,7 @@ def test_shorten_date() -> None:
|
||||
|
||||
|
||||
def test_file_dump_json(mocker) -> None:
|
||||
file_open = mocker.patch('freqtrade.misc.open', MagicMock())
|
||||
file_open = mocker.patch('freqtrade.misc.Path.open', MagicMock())
|
||||
json_dump = mocker.patch('rapidjson.dump', MagicMock())
|
||||
file_dump_json(Path('somefile'), [1, 2, 3])
|
||||
assert file_open.call_count == 1
|
||||
@@ -231,3 +232,7 @@ def test_dataframe_json(ohlcv_history):
|
||||
assert len(ohlcv_history) == len(dataframe)
|
||||
|
||||
assert_frame_equal(ohlcv_history, dataframe)
|
||||
ohlcv_history.at[1, 'date'] = pd.NaT
|
||||
json = dataframe_to_json(ohlcv_history)
|
||||
|
||||
dataframe = json_to_dataframe(json)
|
||||
|
@@ -45,8 +45,7 @@ def test_init_plotscript(default_conf, mocker, testdatadir):
|
||||
default_conf['timerange'] = "20180110-20180112"
|
||||
default_conf['trade_source'] = "file"
|
||||
default_conf['timeframe'] = "5m"
|
||||
default_conf["datadir"] = testdatadir
|
||||
default_conf['exportfilename'] = testdatadir / "backtest-result_new.json"
|
||||
default_conf['exportfilename'] = testdatadir / "backtest-result.json"
|
||||
supported_markets = ["TRX/BTC", "ADA/BTC"]
|
||||
ret = init_plotscript(default_conf, supported_markets)
|
||||
assert "ohlcv" in ret
|
||||
@@ -158,7 +157,7 @@ def test_plot_trades(testdatadir, caplog):
|
||||
assert fig == fig1
|
||||
assert log_has("No trades found.", caplog)
|
||||
pair = "ADA/BTC"
|
||||
filename = testdatadir / "backtest_results/backtest-result_new.json"
|
||||
filename = testdatadir / "backtest_results/backtest-result.json"
|
||||
trades = load_backtest_data(filename)
|
||||
trades = trades.loc[trades['pair'] == pair]
|
||||
|
||||
@@ -299,7 +298,7 @@ def test_generate_plot_file(mocker, caplog):
|
||||
|
||||
|
||||
def test_add_profit(testdatadir):
|
||||
filename = testdatadir / "backtest_results/backtest-result_new.json"
|
||||
filename = testdatadir / "backtest_results/backtest-result.json"
|
||||
bt_data = load_backtest_data(filename)
|
||||
timerange = TimeRange.parse_timerange("20180110-20180112")
|
||||
|
||||
@@ -319,7 +318,7 @@ def test_add_profit(testdatadir):
|
||||
|
||||
|
||||
def test_generate_profit_graph(testdatadir):
|
||||
filename = testdatadir / "backtest_results/backtest-result_new.json"
|
||||
filename = testdatadir / "backtest_results/backtest-result.json"
|
||||
trades = load_backtest_data(filename)
|
||||
timerange = TimeRange.parse_timerange("20180110-20180112")
|
||||
pairs = ["TRX/BTC", "XLM/BTC"]
|
||||
@@ -354,7 +353,7 @@ def test_generate_profit_graph(testdatadir):
|
||||
|
||||
profit = find_trace_in_fig_data(figure.data, "Profit")
|
||||
assert isinstance(profit, go.Scatter)
|
||||
drawdown = find_trace_in_fig_data(figure.data, "Max drawdown 35.69%")
|
||||
drawdown = find_trace_in_fig_data(figure.data, "Max drawdown 73.89%")
|
||||
assert isinstance(drawdown, go.Scatter)
|
||||
parallel = find_trace_in_fig_data(figure.data, "Parallel trades")
|
||||
assert isinstance(parallel, go.Scatter)
|
||||
@@ -394,8 +393,7 @@ 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_new.json"
|
||||
default_conf['exportfilename'] = testdatadir / "backtest-result.json"
|
||||
default_conf['indicators1'] = ["sma5", "ema10"]
|
||||
default_conf['indicators2'] = ["macd"]
|
||||
default_conf['pairs'] = ["ETH/BTC", "LTC/BTC"]
|
||||
@@ -451,7 +449,6 @@ def test_start_plot_profit_error(mocker):
|
||||
def test_plot_profit(default_conf, mocker, testdatadir):
|
||||
patch_exchange(mocker)
|
||||
default_conf['trade_source'] = 'file'
|
||||
default_conf['datadir'] = testdatadir
|
||||
default_conf['exportfilename'] = testdatadir / 'backtest-result_test_nofile.json'
|
||||
default_conf['pairs'] = ['ETH/BTC', 'LTC/BTC']
|
||||
|
||||
@@ -466,7 +463,7 @@ def test_plot_profit(default_conf, mocker, testdatadir):
|
||||
match=r"No trades found, cannot generate Profit-plot.*"):
|
||||
plot_profit(default_conf)
|
||||
|
||||
default_conf['exportfilename'] = testdatadir / "backtest_results/backtest-result_new.json"
|
||||
default_conf['exportfilename'] = testdatadir / "backtest_results/backtest-result.json"
|
||||
|
||||
plot_profit(default_conf)
|
||||
|
||||
|
214
tests/test_strategy_updater.py
Normal file
214
tests/test_strategy_updater.py
Normal file
@@ -0,0 +1,214 @@
|
||||
# pragma pylint: disable=missing-docstring, protected-access, invalid-name
|
||||
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from freqtrade.commands.strategy_utils_commands import start_strategy_update
|
||||
from freqtrade.strategy.strategyupdater import StrategyUpdater
|
||||
from tests.conftest import get_args
|
||||
|
||||
|
||||
if sys.version_info < (3, 9):
|
||||
pytest.skip("StrategyUpdater is not compatible with Python 3.8", allow_module_level=True)
|
||||
|
||||
|
||||
def test_strategy_updater_start(tmpdir, capsys) -> None:
|
||||
# Effective test without mocks.
|
||||
teststrats = Path(__file__).parent / 'strategy/strats'
|
||||
tmpdirp = Path(tmpdir) / "strategies"
|
||||
tmpdirp.mkdir()
|
||||
shutil.copy(teststrats / 'strategy_test_v2.py', tmpdirp)
|
||||
old_code = (teststrats / 'strategy_test_v2.py').read_text()
|
||||
|
||||
args = [
|
||||
"strategy-updater",
|
||||
"--userdir",
|
||||
str(tmpdir),
|
||||
"--strategy-list",
|
||||
"StrategyTestV2"
|
||||
]
|
||||
pargs = get_args(args)
|
||||
pargs['config'] = None
|
||||
|
||||
start_strategy_update(pargs)
|
||||
|
||||
assert Path(tmpdir / "strategies_orig_updater").exists()
|
||||
# Backup file exists
|
||||
assert Path(tmpdir / "strategies_orig_updater" / 'strategy_test_v2.py').exists()
|
||||
# updated file exists
|
||||
new_file = Path(tmpdirp / 'strategy_test_v2.py')
|
||||
assert new_file.exists()
|
||||
new_code = new_file.read_text()
|
||||
assert 'INTERFACE_VERSION = 3' in new_code
|
||||
assert 'INTERFACE_VERSION = 2' in old_code
|
||||
captured = capsys.readouterr()
|
||||
|
||||
assert 'Conversion of strategy_test_v2.py started.' in captured.out
|
||||
assert re.search(r'Conversion of strategy_test_v2\.py took .* seconds', captured.out)
|
||||
|
||||
|
||||
def test_strategy_updater_methods(default_conf, caplog) -> None:
|
||||
|
||||
instance_strategy_updater = StrategyUpdater()
|
||||
modified_code1 = instance_strategy_updater.update_code("""
|
||||
class testClass(IStrategy):
|
||||
def populate_buy_trend():
|
||||
pass
|
||||
def populate_sell_trend():
|
||||
pass
|
||||
def check_buy_timeout():
|
||||
pass
|
||||
def check_sell_timeout():
|
||||
pass
|
||||
def custom_sell():
|
||||
pass
|
||||
""")
|
||||
|
||||
assert "populate_entry_trend" in modified_code1
|
||||
assert "populate_exit_trend" in modified_code1
|
||||
assert "check_entry_timeout" in modified_code1
|
||||
assert "check_exit_timeout" in modified_code1
|
||||
assert "custom_exit" in modified_code1
|
||||
assert "INTERFACE_VERSION = 3" in modified_code1
|
||||
|
||||
|
||||
def test_strategy_updater_params(default_conf, caplog) -> None:
|
||||
instance_strategy_updater = StrategyUpdater()
|
||||
|
||||
modified_code2 = instance_strategy_updater.update_code("""
|
||||
ticker_interval = '15m'
|
||||
buy_some_parameter = IntParameter(space='buy')
|
||||
sell_some_parameter = IntParameter(space='sell')
|
||||
""")
|
||||
|
||||
assert "timeframe" in modified_code2
|
||||
# check for not editing hyperopt spaces
|
||||
assert "space='buy'" in modified_code2
|
||||
assert "space='sell'" in modified_code2
|
||||
|
||||
|
||||
def test_strategy_updater_constants(default_conf, caplog) -> None:
|
||||
instance_strategy_updater = StrategyUpdater()
|
||||
modified_code3 = instance_strategy_updater.update_code("""
|
||||
use_sell_signal = True
|
||||
sell_profit_only = True
|
||||
sell_profit_offset = True
|
||||
ignore_roi_if_buy_signal = True
|
||||
forcebuy_enable = True
|
||||
""")
|
||||
|
||||
assert "use_exit_signal" in modified_code3
|
||||
assert "exit_profit_only" in modified_code3
|
||||
assert "exit_profit_offset" in modified_code3
|
||||
assert "ignore_roi_if_entry_signal" in modified_code3
|
||||
assert "force_entry_enable" in modified_code3
|
||||
|
||||
|
||||
def test_strategy_updater_df_columns(default_conf, caplog) -> None:
|
||||
instance_strategy_updater = StrategyUpdater()
|
||||
modified_code = instance_strategy_updater.update_code("""
|
||||
dataframe.loc[reduce(lambda x, y: x & y, conditions), ["buy", "buy_tag"]] = (1, "buy_signal_1")
|
||||
dataframe.loc[reduce(lambda x, y: x & y, conditions), 'sell'] = 1
|
||||
""")
|
||||
|
||||
assert "enter_long" in modified_code
|
||||
assert "exit_long" in modified_code
|
||||
assert "enter_tag" in modified_code
|
||||
|
||||
|
||||
def test_strategy_updater_method_params(default_conf, caplog) -> None:
|
||||
instance_strategy_updater = StrategyUpdater()
|
||||
modified_code = instance_strategy_updater.update_code("""
|
||||
def confirm_trade_exit(sell_reason: str):
|
||||
nr_orders = trade.nr_of_successful_buys
|
||||
pass
|
||||
""")
|
||||
assert "exit_reason" in modified_code
|
||||
assert "nr_orders = trade.nr_of_successful_entries" in modified_code
|
||||
|
||||
|
||||
def test_strategy_updater_dicts(default_conf, caplog) -> None:
|
||||
instance_strategy_updater = StrategyUpdater()
|
||||
modified_code = instance_strategy_updater.update_code("""
|
||||
order_time_in_force = {
|
||||
'buy': 'gtc',
|
||||
'sell': 'ioc'
|
||||
}
|
||||
order_types = {
|
||||
'buy': 'limit',
|
||||
'sell': 'market',
|
||||
'stoploss': 'market',
|
||||
'stoploss_on_exchange': False
|
||||
}
|
||||
unfilledtimeout = {
|
||||
'buy': 1,
|
||||
'sell': 2
|
||||
}
|
||||
""")
|
||||
|
||||
assert "'entry': 'gtc'" in modified_code
|
||||
assert "'exit': 'ioc'" in modified_code
|
||||
assert "'entry': 'limit'" in modified_code
|
||||
assert "'exit': 'market'" in modified_code
|
||||
assert "'entry': 1" in modified_code
|
||||
assert "'exit': 2" in modified_code
|
||||
|
||||
|
||||
def test_strategy_updater_comparisons(default_conf, caplog) -> None:
|
||||
instance_strategy_updater = StrategyUpdater()
|
||||
modified_code = instance_strategy_updater.update_code("""
|
||||
def confirm_trade_exit(sell_reason):
|
||||
if (sell_reason == 'stop_loss'):
|
||||
pass
|
||||
""")
|
||||
assert "exit_reason" in modified_code
|
||||
assert "exit_reason == 'stop_loss'" in modified_code
|
||||
|
||||
|
||||
def test_strategy_updater_strings(default_conf, caplog) -> None:
|
||||
instance_strategy_updater = StrategyUpdater()
|
||||
|
||||
modified_code = instance_strategy_updater.update_code("""
|
||||
sell_reason == 'sell_signal'
|
||||
sell_reason == 'force_sell'
|
||||
sell_reason == 'emergency_sell'
|
||||
""")
|
||||
|
||||
# those tests currently don't work, next in line.
|
||||
assert "exit_signal" in modified_code
|
||||
assert "exit_reason" in modified_code
|
||||
assert "force_exit" in modified_code
|
||||
assert "emergency_exit" in modified_code
|
||||
|
||||
|
||||
def test_strategy_updater_comments(default_conf, caplog) -> None:
|
||||
instance_strategy_updater = StrategyUpdater()
|
||||
modified_code = instance_strategy_updater.update_code("""
|
||||
# This is the 1st comment
|
||||
import talib.abstract as ta
|
||||
# This is the 2nd comment
|
||||
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
||||
|
||||
|
||||
class someStrategy(IStrategy):
|
||||
INTERFACE_VERSION = 2
|
||||
# This is the 3rd comment
|
||||
# This attribute will be overridden if the config file contains "minimal_roi"
|
||||
minimal_roi = {
|
||||
"0": 0.50
|
||||
}
|
||||
|
||||
# This is the 4th comment
|
||||
stoploss = -0.1
|
||||
""")
|
||||
|
||||
assert "This is the 1st comment" in modified_code
|
||||
assert "This is the 2nd comment" in modified_code
|
||||
assert "This is the 3rd comment" in modified_code
|
||||
assert "INTERFACE_VERSION = 3" in modified_code
|
||||
# currently still missing:
|
||||
# Webhook terminology, Telegram notification settings, Strategy/Config settings
|
@@ -6,13 +6,13 @@ import pytest
|
||||
|
||||
from freqtrade.constants import UNLIMITED_STAKE_AMOUNT
|
||||
from freqtrade.exceptions import DependencyException
|
||||
from tests.conftest import create_mock_trades, get_patched_freqtradebot, patch_wallet
|
||||
from tests.conftest import EXMS, create_mock_trades, get_patched_freqtradebot, patch_wallet
|
||||
|
||||
|
||||
def test_sync_wallet_at_boot(mocker, default_conf):
|
||||
default_conf['dry_run'] = False
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
get_balances=MagicMock(return_value={
|
||||
"BNT": {
|
||||
"free": 1.0,
|
||||
@@ -45,7 +45,7 @@ def test_sync_wallet_at_boot(mocker, default_conf):
|
||||
assert 'USDT' in freqtrade.wallets._wallets
|
||||
assert freqtrade.wallets._last_wallet_refresh > 0
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
get_balances=MagicMock(return_value={
|
||||
"BNT": {
|
||||
"free": 1.2,
|
||||
@@ -87,7 +87,7 @@ def test_sync_wallet_at_boot(mocker, default_conf):
|
||||
def test_sync_wallet_missing_data(mocker, default_conf):
|
||||
default_conf['dry_run'] = False
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
get_balances=MagicMock(return_value={
|
||||
"BNT": {
|
||||
"free": 1.0,
|
||||
@@ -136,7 +136,7 @@ def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, balance_r
|
||||
result1, result2, limit_buy_order_open,
|
||||
fee, mocker) -> None:
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
fetch_ticker=ticker,
|
||||
create_order=MagicMock(return_value=limit_buy_order_open),
|
||||
get_fee=fee
|
||||
@@ -180,17 +180,17 @@ def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, balance_r
|
||||
assert result == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize('stake_amount,min_stake,stake_available,max_stake,expected', [
|
||||
(22, 11, 50, 10000, 22),
|
||||
(100, 11, 500, 10000, 100),
|
||||
(1000, 11, 500, 10000, 500), # Above stake_available
|
||||
(700, 11, 1000, 400, 400), # Above max_stake, below stake available
|
||||
(20, 15, 10, 10000, 0), # Minimum stake > stake_available
|
||||
(9, 11, 100, 10000, 11), # Below min stake
|
||||
(1, 15, 10, 10000, 0), # Below min stake and min_stake > stake_available
|
||||
(20, 50, 100, 10000, 0), # Below min stake and stake * 1.3 > min_stake
|
||||
(1000, None, 1000, 10000, 1000), # No min-stake-amount could be determined
|
||||
|
||||
@pytest.mark.parametrize('stake_amount,min_stake,stake_available,max_stake,trade_amount,expected', [
|
||||
(22, 11, 50, 10000, None, 22),
|
||||
(100, 11, 500, 10000, None, 100),
|
||||
(1000, 11, 500, 10000, None, 500), # Above stake_available
|
||||
(700, 11, 1000, 400, None, 400), # Above max_stake, below stake available
|
||||
(20, 15, 10, 10000, None, 0), # Minimum stake > stake_available
|
||||
(9, 11, 100, 10000, None, 11), # Below min stake
|
||||
(1, 15, 10, 10000, None, 0), # Below min stake and min_stake > stake_available
|
||||
(20, 50, 100, 10000, None, 0), # Below min stake and stake * 1.3 > min_stake
|
||||
(1000, None, 1000, 10000, None, 1000), # No min-stake-amount could be determined
|
||||
(2000, 15, 2000, 3000, 1500, 1500), # Rebuy - resulting in too high stake amount. Adjusting.
|
||||
])
|
||||
def test_validate_stake_amount(
|
||||
mocker,
|
||||
@@ -199,13 +199,15 @@ def test_validate_stake_amount(
|
||||
min_stake,
|
||||
stake_available,
|
||||
max_stake,
|
||||
trade_amount,
|
||||
expected,
|
||||
):
|
||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||
|
||||
mocker.patch("freqtrade.wallets.Wallets.get_available_stake_amount",
|
||||
return_value=stake_available)
|
||||
res = freqtrade.wallets.validate_stake_amount('XRP/USDT', stake_amount, min_stake, max_stake)
|
||||
res = freqtrade.wallets.validate_stake_amount(
|
||||
'XRP/USDT', stake_amount, min_stake, max_stake, trade_amount)
|
||||
assert res == expected
|
||||
|
||||
|
||||
@@ -310,7 +312,7 @@ def test_sync_wallet_futures_live(mocker, default_conf):
|
||||
}
|
||||
]
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
EXMS,
|
||||
get_balances=MagicMock(return_value={
|
||||
"USDT": {
|
||||
"free": 900,
|
||||
|
@@ -8,11 +8,11 @@ import time_machine
|
||||
from freqtrade.data.dataprovider import DataProvider
|
||||
from freqtrade.enums import State
|
||||
from freqtrade.worker import Worker
|
||||
from tests.conftest import get_patched_worker, log_has, log_has_re
|
||||
from tests.conftest import EXMS, get_patched_worker, log_has, log_has_re
|
||||
|
||||
|
||||
def test_worker_state(mocker, default_conf, markets) -> None:
|
||||
mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets))
|
||||
mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets))
|
||||
worker = get_patched_worker(mocker, default_conf)
|
||||
assert worker.freqtrade.state is State.RUNNING
|
||||
|
||||
|
@@ -1 +1 @@
|
||||
{"latest_backtest":"backtest-result_new.json"}
|
||||
{"latest_backtest":"backtest-result.json"}
|
||||
|
1
tests/testdata/backtest_results/backtest-result.json
vendored
Normal file
1
tests/testdata/backtest_results/backtest-result.json
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
tests/testdata/futures/XRP_USDT_USDT-5m-futures.json
vendored
Normal file
1
tests/testdata/futures/XRP_USDT_USDT-5m-futures.json
vendored
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user