Merge remote-tracking branch 'origin/develop' into feature/proceed-exit-while-open-order

This commit is contained in:
Axel-CH
2024-09-24 11:58:15 -04:00
57 changed files with 4549 additions and 2495 deletions
-7
View File
@@ -90,13 +90,6 @@ def test_historic_trades(mocker, default_conf, trades_history_df):
assert isinstance(data, DataFrame)
assert len(data) == len(trades_history_df)
# Random other runmode
default_conf["runmode"] = RunMode.UTIL_EXCHANGE
dp = DataProvider(default_conf, None)
data = dp.trades("UNITTEST/BTC", "5m")
assert isinstance(data, DataFrame)
assert len(data) == 0
def test_historic_ohlcv_dataformat(mocker, default_conf, ohlcv_history):
hdf5loadmock = MagicMock(return_value=ohlcv_history)
+316 -1
View File
@@ -7,6 +7,7 @@ import pytest
from freqtrade.commands.analyze_commands import start_analysis_entries_exits
from freqtrade.commands.optimize_commands import start_backtesting
from freqtrade.enums import ExitType
from freqtrade.exceptions import OperationalException
from freqtrade.optimize.backtesting import Backtesting
from tests.conftest import get_args, patch_exchange, patched_configuration_load_config_file
@@ -18,7 +19,9 @@ def entryexitanalysis_cleanup() -> None:
Backtesting.cleanup()
def test_backtest_analysis_nomock(default_conf, mocker, caplog, testdatadir, user_dir, capsys):
def test_backtest_analysis_on_entry_and_rejected_signals_nomock(
default_conf, mocker, caplog, testdatadir, user_dir, capsys
):
caplog.set_level(logging.INFO)
(user_dir / "backtest_results").mkdir(parents=True, exist_ok=True)
@@ -158,6 +161,15 @@ def test_backtest_analysis_nomock(default_conf, mocker, caplog, testdatadir, use
assert "34.049" in captured.out
assert "0.104" in captured.out
assert "52.829" in captured.out
# assert indicator list
assert "close (entry)" in captured.out
assert "0.016" in captured.out
assert "rsi (entry)" in captured.out
assert "54.320" in captured.out
assert "close (exit)" in captured.out
assert "rsi (exit)" in captured.out
assert "52.829" in captured.out
assert "profit_abs" in captured.out
# test group 1
args = get_args(base_args + ["--analysis-groups", "1"])
@@ -245,3 +257,306 @@ def test_backtest_analysis_nomock(default_conf, mocker, caplog, testdatadir, use
start_analysis_entries_exits(args)
captured = capsys.readouterr()
assert "no rejected signals" in captured.out
def test_backtest_analysis_with_invalid_config(
default_conf, mocker, caplog, testdatadir, user_dir, capsys
):
caplog.set_level(logging.INFO)
(user_dir / "backtest_results").mkdir(parents=True, exist_ok=True)
default_conf.update(
{
"use_exit_signal": True,
"exit_profit_only": False,
"exit_profit_offset": 0.0,
"ignore_roi_if_entry_signal": False,
}
)
patch_exchange(mocker)
result1 = pd.DataFrame(
{
"pair": ["ETH/BTC", "LTC/BTC", "ETH/BTC", "LTC/BTC"],
"profit_ratio": [0.025, 0.05, -0.1, -0.05],
"profit_abs": [0.5, 2.0, -4.0, -2.0],
"open_date": pd.to_datetime(
[
"2018-01-29 18:40:00",
"2018-01-30 03:30:00",
"2018-01-30 08:10:00",
"2018-01-31 13:30:00",
],
utc=True,
),
"close_date": pd.to_datetime(
[
"2018-01-29 20:45:00",
"2018-01-30 05:35:00",
"2018-01-30 09:10:00",
"2018-01-31 15:00:00",
],
utc=True,
),
"trade_duration": [235, 40, 60, 90],
"is_open": [False, False, False, False],
"stake_amount": [0.01, 0.01, 0.01, 0.01],
"open_rate": [0.104445, 0.10302485, 0.10302485, 0.10302485],
"close_rate": [0.104969, 0.103541, 0.102041, 0.102541],
"is_short": [False, False, False, False],
"enter_tag": [
"enter_tag_long_a",
"enter_tag_long_b",
"enter_tag_long_a",
"enter_tag_long_b",
],
"exit_reason": [
ExitType.ROI.value,
ExitType.EXIT_SIGNAL.value,
ExitType.STOP_LOSS.value,
ExitType.TRAILING_STOP_LOSS.value,
],
}
)
backtestmock = MagicMock(
side_effect=[
{
"results": result1,
"config": default_conf,
"locks": [],
"rejected_signals": 20,
"timedout_entry_orders": 0,
"timedout_exit_orders": 0,
"canceled_trade_entries": 0,
"canceled_entry_orders": 0,
"replaced_entry_orders": 0,
"final_balance": 1000,
}
]
)
mocker.patch(
"freqtrade.plugins.pairlistmanager.PairListManager.whitelist",
PropertyMock(return_value=["ETH/BTC", "LTC/BTC", "DASH/BTC"]),
)
mocker.patch("freqtrade.optimize.backtesting.Backtesting.backtest", backtestmock)
patched_configuration_load_config_file(mocker, default_conf)
args = [
"backtesting",
"--config",
"config.json",
"--datadir",
str(testdatadir),
"--user-data-dir",
str(user_dir),
"--timeframe",
"5m",
"--timerange",
"1515560100-1517287800",
"--export",
"signals",
"--cache",
"none",
]
args = get_args(args)
start_backtesting(args)
captured = capsys.readouterr()
assert "BACKTESTING REPORT" in captured.out
assert "EXIT REASON STATS" in captured.out
assert "LEFT OPEN TRADES REPORT" in captured.out
base_args = [
"backtesting-analysis",
"--config",
"config.json",
"--datadir",
str(testdatadir),
"--user-data-dir",
str(user_dir),
]
# test with both entry and exit only arguments
args = get_args(
base_args
+ [
"--analysis-groups",
"0",
"--indicator-list",
"close",
"rsi",
"profit_abs",
"--entry-only",
"--exit-only",
]
)
with pytest.raises(
OperationalException,
match=r"Cannot use --entry-only and --exit-only at the same time. Please choose one.",
):
start_analysis_entries_exits(args)
def test_backtest_analysis_on_entry_and_rejected_signals_only_entry_signals(
default_conf, mocker, caplog, testdatadir, user_dir, capsys
):
caplog.set_level(logging.INFO)
(user_dir / "backtest_results").mkdir(parents=True, exist_ok=True)
default_conf.update(
{
"use_exit_signal": True,
"exit_profit_only": False,
"exit_profit_offset": 0.0,
"ignore_roi_if_entry_signal": False,
}
)
patch_exchange(mocker)
result1 = pd.DataFrame(
{
"pair": ["ETH/BTC", "LTC/BTC", "ETH/BTC", "LTC/BTC"],
"profit_ratio": [0.025, 0.05, -0.1, -0.05],
"profit_abs": [0.5, 2.0, -4.0, -2.0],
"open_date": pd.to_datetime(
[
"2018-01-29 18:40:00",
"2018-01-30 03:30:00",
"2018-01-30 08:10:00",
"2018-01-31 13:30:00",
],
utc=True,
),
"close_date": pd.to_datetime(
[
"2018-01-29 20:45:00",
"2018-01-30 05:35:00",
"2018-01-30 09:10:00",
"2018-01-31 15:00:00",
],
utc=True,
),
"trade_duration": [235, 40, 60, 90],
"is_open": [False, False, False, False],
"stake_amount": [0.01, 0.01, 0.01, 0.01],
"open_rate": [0.104445, 0.10302485, 0.10302485, 0.10302485],
"close_rate": [0.104969, 0.103541, 0.102041, 0.102541],
"is_short": [False, False, False, False],
"enter_tag": [
"enter_tag_long_a",
"enter_tag_long_b",
"enter_tag_long_a",
"enter_tag_long_b",
],
"exit_reason": [
ExitType.ROI.value,
ExitType.EXIT_SIGNAL.value,
ExitType.STOP_LOSS.value,
ExitType.TRAILING_STOP_LOSS.value,
],
}
)
backtestmock = MagicMock(
side_effect=[
{
"results": result1,
"config": default_conf,
"locks": [],
"rejected_signals": 20,
"timedout_entry_orders": 0,
"timedout_exit_orders": 0,
"canceled_trade_entries": 0,
"canceled_entry_orders": 0,
"replaced_entry_orders": 0,
"final_balance": 1000,
}
]
)
mocker.patch(
"freqtrade.plugins.pairlistmanager.PairListManager.whitelist",
PropertyMock(return_value=["ETH/BTC", "LTC/BTC", "DASH/BTC"]),
)
mocker.patch("freqtrade.optimize.backtesting.Backtesting.backtest", backtestmock)
patched_configuration_load_config_file(mocker, default_conf)
args = [
"backtesting",
"--config",
"config.json",
"--datadir",
str(testdatadir),
"--user-data-dir",
str(user_dir),
"--timeframe",
"5m",
"--timerange",
"1515560100-1517287800",
"--export",
"signals",
"--cache",
"none",
]
args = get_args(args)
start_backtesting(args)
captured = capsys.readouterr()
assert "BACKTESTING REPORT" in captured.out
assert "EXIT REASON STATS" in captured.out
assert "LEFT OPEN TRADES REPORT" in captured.out
base_args = [
"backtesting-analysis",
"--config",
"config.json",
"--datadir",
str(testdatadir),
"--user-data-dir",
str(user_dir),
]
# test group 0 and indicator list
args = get_args(
base_args
+ [
"--analysis-groups",
"0",
"--indicator-list",
"close",
"rsi",
"profit_abs",
"--entry-only",
]
)
start_analysis_entries_exits(args)
captured = capsys.readouterr()
assert "LTC/BTC" in captured.out
assert "ETH/BTC" in captured.out
assert "enter_tag_long_a" in captured.out
assert "enter_tag_long_b" in captured.out
assert "exit_signal" in captured.out
assert "roi" in captured.out
assert "stop_loss" in captured.out
assert "trailing_stop_loss" in captured.out
assert "0.5" in captured.out
assert "-4" in captured.out
assert "-2" in captured.out
assert "-3.5" in captured.out
assert "50" in captured.out
assert "0" in captured.out
assert "0.016" in captured.out
assert "34.049" in captured.out
assert "0.104" in captured.out
assert "52.829" in captured.out
# assert indicator list
assert "close" in captured.out
assert "close (entry)" not in captured.out
assert "0.016" in captured.out
assert "rsi (entry)" not in captured.out
assert "rsi" in captured.out
assert "54.320" in captured.out
assert "close (exit)" not in captured.out
assert "rsi (exit)" not in captured.out
assert "52.829" in captured.out
assert "profit_abs" in captured.out
+6 -10
View File
@@ -123,12 +123,12 @@ def test_load_data_startup_candles(mocker, testdatadir) -> None:
@pytest.mark.parametrize("candle_type", ["mark", ""])
def test_load_data_with_new_pair_1min(
ohlcv_history_list, mocker, caplog, default_conf, tmp_path, candle_type
ohlcv_history, mocker, caplog, default_conf, tmp_path, candle_type
) -> None:
"""
Test load_pair_history() with 1 min timeframe
"""
mocker.patch(f"{EXMS}.get_historic_ohlcv", return_value=ohlcv_history_list)
mocker.patch(f"{EXMS}.get_historic_ohlcv", return_value=ohlcv_history)
exchange = get_patched_exchange(mocker, default_conf)
file = tmp_path / "MEME_BTC-1m.feather"
@@ -303,9 +303,9 @@ def test_load_cached_data_for_updating(mocker, testdatadir) -> None:
],
)
def test_download_pair_history(
ohlcv_history_list, mocker, default_conf, tmp_path, candle_type, subdir, file_tail
ohlcv_history, mocker, default_conf, tmp_path, candle_type, subdir, file_tail
) -> None:
mocker.patch(f"{EXMS}.get_historic_ohlcv", return_value=ohlcv_history_list)
mocker.patch(f"{EXMS}.get_historic_ohlcv", return_value=ohlcv_history)
exchange = get_patched_exchange(mocker, default_conf)
file1_1 = tmp_path / f"{subdir}MEME_BTC-1m{file_tail}.feather"
file1_5 = tmp_path / f"{subdir}MEME_BTC-5m{file_tail}.feather"
@@ -351,16 +351,12 @@ def test_download_pair_history(
assert file2_5.is_file()
def test_download_pair_history2(mocker, default_conf, testdatadir) -> None:
tick = [
[1509836520000, 0.00162008, 0.00162008, 0.00162008, 0.00162008, 108.14853839],
[1509836580000, 0.00161, 0.00161, 0.00161, 0.00161, 82.390199],
]
def test_download_pair_history2(mocker, default_conf, testdatadir, ohlcv_history) -> None:
json_dump_mock = mocker.patch(
"freqtrade.data.history.datahandlers.featherdatahandler.FeatherDataHandler.ohlcv_store",
return_value=None,
)
mocker.patch(f"{EXMS}.get_historic_ohlcv", return_value=tick)
mocker.patch(f"{EXMS}.get_historic_ohlcv", return_value=ohlcv_history)
exchange = get_patched_exchange(mocker, default_conf)
_download_pair_history(
datadir=testdatadir,
+5 -10
View File
@@ -1,11 +1,8 @@
from datetime import datetime, timedelta, timezone
from unittest.mock import MagicMock
import pytest
from freqtrade.enums.marginmode import MarginMode
from freqtrade.enums.tradingmode import TradingMode
from freqtrade.exceptions import OperationalException
from tests.conftest import EXMS, get_mock_coro, get_patched_exchange, log_has
from tests.exchange.test_exchange import ccxt_exceptionhandlers
@@ -27,13 +24,11 @@ def test_additional_exchange_init_bybit(default_conf, mocker, caplog):
api_mock.set_position_mode.reset_mock()
api_mock.is_unified_enabled = MagicMock(return_value=[False, True])
with pytest.raises(OperationalException, match=r"Bybit: Unified account is not supported.*"):
get_patched_exchange(mocker, default_conf, exchange="bybit", api_mock=api_mock)
assert log_has("Bybit: Unified account.", caplog)
# exchange = get_patched_exchange(mocker, default_conf, exchange="bybit", api_mock=api_mock)
# assert api_mock.set_position_mode.call_count == 1
# assert api_mock.is_unified_enabled.call_count == 1
# assert exchange.unified_account is True
exchange = get_patched_exchange(mocker, default_conf, exchange="bybit", api_mock=api_mock)
assert log_has("Bybit: Unified account. Assuming dedicated subaccount for this bot.", caplog)
assert api_mock.set_position_mode.call_count == 1
assert api_mock.is_unified_enabled.call_count == 1
assert exchange.unified_account is True
ccxt_exceptionhandlers(
mocker, default_conf, api_mock, "bybit", "additional_exchange_init", "set_position_mode"
+42 -173
View File
@@ -255,7 +255,6 @@ def test_init_exception(default_conf, mocker):
def test_exchange_resolver(default_conf, mocker, caplog):
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=MagicMock()))
mocker.patch(f"{EXMS}._load_async_markets")
mocker.patch(f"{EXMS}.validate_pairs")
mocker.patch(f"{EXMS}.validate_timeframes")
mocker.patch(f"{EXMS}.validate_stakecurrency")
mocker.patch(f"{EXMS}.validate_pricing")
@@ -555,7 +554,6 @@ def test_get_min_pair_stake_amount_real_data(mocker, default_conf) -> None:
def test__load_async_markets(default_conf, mocker, caplog):
mocker.patch(f"{EXMS}._init_ccxt")
mocker.patch(f"{EXMS}.validate_pairs")
mocker.patch(f"{EXMS}.validate_timeframes")
mocker.patch(f"{EXMS}.reload_markets")
mocker.patch(f"{EXMS}.validate_stakecurrency")
@@ -567,7 +565,15 @@ def test__load_async_markets(default_conf, mocker, caplog):
caplog.set_level(logging.DEBUG)
exchange._api_async.load_markets = get_mock_coro(side_effect=ccxt.BaseError("deadbeef"))
with pytest.raises(ccxt.BaseError, match="deadbeef"):
with pytest.raises(TemporaryError, match="deadbeef"):
exchange._load_async_markets()
exchange._api_async.load_markets = get_mock_coro(side_effect=ccxt.DDoSProtection("deadbeef"))
with pytest.raises(DDosProtection, match="deadbeef"):
exchange._load_async_markets()
exchange._api_async.load_markets = get_mock_coro(side_effect=ccxt.OperationFailed("deadbeef"))
with pytest.raises(TemporaryError, match="deadbeef"):
exchange._load_async_markets()
@@ -576,7 +582,6 @@ def test__load_markets(default_conf, mocker, caplog):
api_mock = MagicMock()
api_mock.load_markets = get_mock_coro(side_effect=ccxt.BaseError("SomeError"))
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}.validate_pairs")
mocker.patch(f"{EXMS}.validate_timeframes")
mocker.patch(f"{EXMS}.validate_stakecurrency")
mocker.patch(f"{EXMS}.validate_pricing")
@@ -630,6 +635,21 @@ def test_reload_markets(default_conf, mocker, caplog, time_machine):
exchange.reload_markets()
assert lam_spy.call_count == 0
# Another reload should happen but it fails.
time_machine.move_to(start_dt + timedelta(minutes=51), tick=False)
api_mock.load_markets = get_mock_coro(side_effect=ccxt.NetworkError("LoadError"))
exchange.reload_markets(force=False)
assert exchange.markets == updated_markets
assert lam_spy.call_count == 1
# Tried once, failed
lam_spy.reset_mock()
# When forceing (bot startup), it should retry 3 times.
exchange.reload_markets(force=True)
assert lam_spy.call_count == 4
assert exchange.markets == updated_markets
def test_reload_markets_exception(default_conf, mocker, caplog):
caplog.set_level(logging.DEBUG)
@@ -661,7 +681,6 @@ def test_validate_stakecurrency(default_conf, stake_currency, mocker, caplog):
}
)
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}.validate_pairs")
mocker.patch(f"{EXMS}.validate_timeframes")
mocker.patch(f"{EXMS}.validate_pricing")
Exchange(default_conf)
@@ -679,7 +698,6 @@ def test_validate_stakecurrency_error(default_conf, mocker, caplog):
}
)
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}.validate_pairs")
mocker.patch(f"{EXMS}.validate_timeframes")
with pytest.raises(
ConfigurationError,
@@ -732,147 +750,6 @@ def test_get_pair_base_currency(default_conf, mocker, pair, expected):
assert ex.get_pair_base_currency(pair) == expected
def test_validate_pairs(default_conf, mocker):
api_mock = MagicMock()
id_mock = PropertyMock(return_value="test_exchange")
type(api_mock).id = id_mock
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}.validate_timeframes")
mocker.patch(
f"{EXMS}._load_async_markets",
return_value={
"ETH/BTC": {"quote": "BTC"},
"LTC/BTC": {"quote": "BTC"},
"XRP/BTC": {"quote": "BTC"},
"NEO/BTC": {"quote": "BTC"},
},
)
mocker.patch(f"{EXMS}.validate_stakecurrency")
mocker.patch(f"{EXMS}.validate_pricing")
# test exchange.validate_pairs directly
# No assert - but this should not fail (!)
Exchange(default_conf)
def test_validate_pairs_not_available(default_conf, mocker):
api_mock = MagicMock()
type(api_mock).markets = PropertyMock(
return_value={"XRP/BTC": {"inactive": True, "base": "XRP", "quote": "BTC"}}
)
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}.validate_timeframes")
mocker.patch(f"{EXMS}.validate_stakecurrency")
mocker.patch(f"{EXMS}._load_async_markets")
with pytest.raises(OperationalException, match=r"not available"):
Exchange(default_conf)
def test_validate_pairs_exception(default_conf, mocker, caplog):
caplog.set_level(logging.INFO)
api_mock = MagicMock()
mocker.patch(f"{EXMS}.name", PropertyMock(return_value="Binance"))
type(api_mock).markets = PropertyMock(return_value={})
mocker.patch(f"{EXMS}._init_ccxt", api_mock)
mocker.patch(f"{EXMS}.validate_timeframes")
mocker.patch(f"{EXMS}.validate_stakecurrency")
mocker.patch(f"{EXMS}.validate_pricing")
mocker.patch(f"{EXMS}._load_async_markets")
with pytest.raises(OperationalException, match=r"Pair ETH/BTC is not available on Binance"):
Exchange(default_conf)
mocker.patch(f"{EXMS}.markets", PropertyMock(return_value={}))
Exchange(default_conf)
assert log_has("Unable to validate pairs (assuming they are correct).", caplog)
def test_validate_pairs_restricted(default_conf, mocker, caplog):
api_mock = MagicMock()
type(api_mock).load_markets = get_mock_coro(
return_value={
"ETH/BTC": {"quote": "BTC"},
"LTC/BTC": {"quote": "BTC"},
"XRP/BTC": {"quote": "BTC", "info": {"prohibitedIn": ["US"]}},
"NEO/BTC": {"quote": "BTC", "info": "TestString"}, # info can also be a string ...
}
)
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}.validate_timeframes")
mocker.patch(f"{EXMS}.validate_pricing")
mocker.patch(f"{EXMS}.validate_stakecurrency")
Exchange(default_conf)
assert log_has(
"Pair XRP/BTC is restricted for some users on this exchange."
"Please check if you are impacted by this restriction "
"on the exchange and eventually remove XRP/BTC from your whitelist.",
caplog,
)
def test_validate_pairs_stakecompatibility(default_conf, mocker):
api_mock = MagicMock()
type(api_mock).load_markets = get_mock_coro(
return_value={
"ETH/BTC": {"quote": "BTC"},
"LTC/BTC": {"quote": "BTC"},
"XRP/BTC": {"quote": "BTC"},
"NEO/BTC": {"quote": "BTC"},
"HELLO-WORLD": {"quote": "BTC"},
}
)
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}.validate_timeframes")
mocker.patch(f"{EXMS}.validate_stakecurrency")
mocker.patch(f"{EXMS}.validate_pricing")
Exchange(default_conf)
def test_validate_pairs_stakecompatibility_downloaddata(default_conf, mocker):
api_mock = MagicMock()
default_conf["stake_currency"] = ""
type(api_mock).load_markets = get_mock_coro(
return_value={
"ETH/BTC": {"quote": "BTC"},
"LTC/BTC": {"quote": "BTC"},
"XRP/BTC": {"quote": "BTC"},
"NEO/BTC": {"quote": "BTC"},
"HELLO-WORLD": {"quote": "BTC"},
}
)
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}.validate_timeframes")
mocker.patch(f"{EXMS}.validate_stakecurrency")
mocker.patch(f"{EXMS}.validate_pricing")
Exchange(default_conf)
assert type(api_mock).load_markets.call_count == 1
def test_validate_pairs_stakecompatibility_fail(default_conf, mocker):
default_conf["exchange"]["pair_whitelist"].append("HELLO-WORLD")
api_mock = MagicMock()
type(api_mock).load_markets = get_mock_coro(
return_value={
"ETH/BTC": {"quote": "BTC"},
"LTC/BTC": {"quote": "BTC"},
"XRP/BTC": {"quote": "BTC"},
"NEO/BTC": {"quote": "BTC"},
"HELLO-WORLD": {"quote": "USDT"},
}
)
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}.validate_timeframes")
mocker.patch(f"{EXMS}.validate_stakecurrency")
with pytest.raises(OperationalException, match=r"Stake-currency 'BTC' not compatible with.*"):
Exchange(default_conf)
@pytest.mark.parametrize("timeframe", [("5m"), ("1m"), ("15m"), ("1h")])
def test_validate_timeframes(default_conf, mocker, timeframe):
default_conf["timeframe"] = timeframe
@@ -884,7 +761,6 @@ def test_validate_timeframes(default_conf, mocker, timeframe):
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}.reload_markets")
mocker.patch(f"{EXMS}.validate_pairs")
mocker.patch(f"{EXMS}.validate_stakecurrency")
mocker.patch(f"{EXMS}.validate_pricing")
Exchange(default_conf)
@@ -902,7 +778,6 @@ def test_validate_timeframes_failed(default_conf, mocker):
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}.reload_markets")
mocker.patch(f"{EXMS}.validate_pairs")
mocker.patch(f"{EXMS}.validate_stakecurrency")
mocker.patch(f"{EXMS}.validate_pricing")
with pytest.raises(
@@ -932,7 +807,6 @@ def test_validate_timeframes_emulated_ohlcv_1(default_conf, mocker):
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}.reload_markets")
mocker.patch(f"{EXMS}.validate_pairs")
mocker.patch(f"{EXMS}.validate_stakecurrency")
with pytest.raises(
OperationalException,
@@ -954,7 +828,6 @@ def test_validate_timeframes_emulated_ohlcvi_2(default_conf, mocker):
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}.reload_markets")
mocker.patch(f"{EXMS}.validate_pairs", MagicMock())
mocker.patch(f"{EXMS}.validate_stakecurrency")
with pytest.raises(
OperationalException,
@@ -976,7 +849,6 @@ def test_validate_timeframes_not_in_config(default_conf, mocker):
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}.reload_markets")
mocker.patch(f"{EXMS}.validate_pairs")
mocker.patch(f"{EXMS}.validate_stakecurrency")
mocker.patch(f"{EXMS}.validate_pricing")
mocker.patch(f"{EXMS}.validate_required_startup_candles")
@@ -993,7 +865,6 @@ def test_validate_pricing(default_conf, mocker):
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}.reload_markets")
mocker.patch(f"{EXMS}.validate_trading_mode_and_margin_mode")
mocker.patch(f"{EXMS}.validate_pairs")
mocker.patch(f"{EXMS}.validate_timeframes")
mocker.patch(f"{EXMS}.validate_stakecurrency")
mocker.patch(f"{EXMS}.name", "Binance")
@@ -1028,7 +899,6 @@ def test_validate_ordertypes(default_conf, mocker):
type(api_mock).has = PropertyMock(return_value={"createMarketOrder": True})
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}.reload_markets")
mocker.patch(f"{EXMS}.validate_pairs")
mocker.patch(f"{EXMS}.validate_timeframes")
mocker.patch(f"{EXMS}.validate_stakecurrency")
mocker.patch(f"{EXMS}.validate_pricing")
@@ -1087,7 +957,6 @@ def test_validate_ordertypes_stop_advanced(default_conf, mocker, exchange_name,
type(api_mock).has = PropertyMock(return_value={"createMarketOrder": True})
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}.reload_markets")
mocker.patch(f"{EXMS}.validate_pairs")
mocker.patch(f"{EXMS}.validate_timeframes")
mocker.patch(f"{EXMS}.validate_stakecurrency")
mocker.patch(f"{EXMS}.validate_pricing")
@@ -1112,7 +981,6 @@ def test_validate_order_types_not_in_config(default_conf, mocker):
api_mock = MagicMock()
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}.reload_markets")
mocker.patch(f"{EXMS}.validate_pairs")
mocker.patch(f"{EXMS}.validate_timeframes")
mocker.patch(f"{EXMS}.validate_pricing")
mocker.patch(f"{EXMS}.validate_stakecurrency")
@@ -1128,7 +996,6 @@ def test_validate_required_startup_candles(default_conf, mocker, caplog):
mocker.patch(f"{EXMS}._init_ccxt", api_mock)
mocker.patch(f"{EXMS}.validate_timeframes")
mocker.patch(f"{EXMS}._load_async_markets")
mocker.patch(f"{EXMS}.validate_pairs")
mocker.patch(f"{EXMS}.validate_pricing")
mocker.patch(f"{EXMS}.validate_stakecurrency")
@@ -2217,24 +2084,29 @@ def test___now_is_time_to_refresh(default_conf, mocker, exchange_name, time_mach
assert exchange._now_is_time_to_refresh(pair, "5m", candle_type) is True
@pytest.mark.parametrize("exchange_name", EXCHANGES)
@pytest.mark.parametrize("candle_type", ["mark", ""])
@pytest.mark.parametrize("exchange_name", EXCHANGES)
def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name, candle_type):
exchange = get_patched_exchange(mocker, default_conf, exchange=exchange_name)
ohlcv = [
[
dt_ts(), # unix timestamp ms
1, # open
2, # high
3, # low
4, # close
5, # volume (in quote currency)
]
]
pair = "ETH/BTC"
calls = 0
now = dt_now()
async def mock_candle_hist(pair, timeframe, candle_type, since_ms):
return pair, timeframe, candle_type, ohlcv, True
nonlocal calls
calls += 1
ohlcv = [
[
dt_ts(now + timedelta(minutes=5 * (calls + i))), # unix timestamp ms
1, # open
2, # high
3, # low
4, # close
5, # volume (in quote currency)
]
for i in range(2)
]
return (pair, timeframe, candle_type, ohlcv, True)
exchange._async_get_candle_history = Mock(wraps=mock_candle_hist)
# one_call calculation * 1.8 should do 2 calls
@@ -2245,7 +2117,7 @@ def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name, candle_
)
assert exchange._async_get_candle_history.call_count == 2
# Returns twice the above OHLCV data
# Returns twice the above OHLCV data after truncating the open candle.
assert len(ret) == 2
assert log_has_re(r"Downloaded data for .* with length .*\.", caplog)
@@ -4162,7 +4034,6 @@ def test_merge_ft_has_dict(default_conf, mocker):
EXMS,
_init_ccxt=MagicMock(return_value=MagicMock()),
_load_async_markets=MagicMock(),
validate_pairs=MagicMock(),
validate_timeframes=MagicMock(),
validate_stakecurrency=MagicMock(),
validate_pricing=MagicMock(),
@@ -4197,7 +4068,6 @@ def test_get_valid_pair_combination(default_conf, mocker, markets):
EXMS,
_init_ccxt=MagicMock(return_value=MagicMock()),
_load_async_markets=MagicMock(),
validate_pairs=MagicMock(),
validate_timeframes=MagicMock(),
validate_pricing=MagicMock(),
markets=PropertyMock(return_value=markets),
@@ -4477,7 +4347,6 @@ def test_get_markets(
EXMS,
_init_ccxt=MagicMock(return_value=MagicMock()),
_load_async_markets=MagicMock(),
validate_pairs=MagicMock(),
validate_timeframes=MagicMock(),
validate_pricing=MagicMock(),
markets=PropertyMock(return_value=markets_static),
+22 -7
View File
@@ -689,13 +689,29 @@ def test_process_trade_creation(
assert trade.open_date is not None
assert trade.exchange == "binance"
assert trade.open_rate == ticker_usdt.return_value[ticker_side]
assert pytest.approx(trade.amount) == 60 / ticker_usdt.return_value[ticker_side]
# Trade opens with 0 amount. Only trade filling will set the amount
assert pytest.approx(trade.amount) == 0
assert pytest.approx(trade.amount_requested) == 60 / ticker_usdt.return_value[ticker_side]
assert log_has(
f'{"Short" if is_short else "Long"} signal found: about create a new trade for ETH/USDT '
"with stake_amount: 60.0 ...",
caplog,
)
mocker.patch("freqtrade.freqtradebot.FreqtradeBot._check_and_execute_exit")
# Fill trade.
freqtrade.process()
trades = Trade.get_open_trades()
assert len(trades) == 1
trade = trades[0]
assert trade is not None
assert trade.is_open
assert trade.open_date is not None
assert trade.exchange == "binance"
assert trade.open_rate == limit_order[entry_side(is_short)]["price"]
# Filled trade has amount set to filled order amount
assert pytest.approx(trade.amount) == limit_order[entry_side(is_short)]["filled"]
def test_process_exchange_failures(default_conf_usdt, ticker_usdt, mocker) -> None:
@@ -1685,7 +1701,7 @@ def test_handle_trade_roi(
create_order=MagicMock(
side_effect=[
open_order,
{"id": 1234553382},
{"id": 1234553382, "amount": open_order["amount"]},
]
),
get_fee=fee,
@@ -2205,7 +2221,6 @@ def test_manage_open_orders_buy_exception(
patch_exchange(mocker)
mocker.patch.multiple(
EXMS,
validate_pairs=MagicMock(),
fetch_ticker=ticker_usdt,
fetch_order=MagicMock(side_effect=ExchangeError),
cancel_order=cancel_order_mock,
@@ -2884,7 +2899,7 @@ def test_execute_trade_exit_up(
EXMS,
fetch_ticker=ticker_usdt,
get_fee=fee,
_dry_is_price_crossed=MagicMock(return_value=False),
_dry_is_price_crossed=MagicMock(side_effect=[True, False]),
)
patch_whitelist(mocker, default_conf_usdt)
freqtrade = FreqtradeBot(default_conf_usdt)
@@ -2976,7 +2991,7 @@ def test_execute_trade_exit_down(
EXMS,
fetch_ticker=ticker_usdt,
get_fee=fee,
_dry_is_price_crossed=MagicMock(return_value=False),
_dry_is_price_crossed=MagicMock(side_effect=[True, False]),
)
patch_whitelist(mocker, default_conf_usdt)
freqtrade = FreqtradeBot(default_conf_usdt)
@@ -2999,7 +3014,7 @@ def test_execute_trade_exit_down(
exit_check=ExitCheckTuple(exit_type=ExitType.STOP_LOSS),
)
assert rpc_mock.call_count == 2
assert rpc_mock.call_count == 3
last_msg = rpc_mock.call_args_list[-1][0][0]
assert {
"type": RPCMessageType.EXIT,
@@ -3063,7 +3078,7 @@ def test_execute_trade_exit_custom_exit_price(
EXMS,
fetch_ticker=ticker_usdt,
get_fee=fee,
_dry_is_price_crossed=MagicMock(return_value=False),
_dry_is_price_crossed=MagicMock(side_effect=[True, False]),
)
config = deepcopy(default_conf_usdt)
config["custom_price_max_distance_ratio"] = 0.1
@@ -1109,7 +1109,7 @@ def test_execute_trade_exit_down_stoploss_on_exchange_dry_run(
EXMS,
fetch_ticker=ticker_usdt,
get_fee=fee,
_dry_is_price_crossed=MagicMock(return_value=False),
_dry_is_price_crossed=MagicMock(side_effect=[True, False]),
)
patch_whitelist(mocker, default_conf_usdt)
freqtrade = FreqtradeBot(default_conf_usdt)
@@ -1136,7 +1136,7 @@ def test_execute_trade_exit_down_stoploss_on_exchange_dry_run(
trade=trade, limit=trade.stop_loss, exit_check=ExitCheckTuple(exit_type=ExitType.STOP_LOSS)
)
assert rpc_mock.call_count == 2
# assert rpc_mock.call_count == 2
last_msg = rpc_mock.call_args_list[-1][0][0]
assert {
@@ -1169,7 +1169,7 @@ def test_execute_trade_exit_down_stoploss_on_exchange_dry_run(
"cumulative_profit": 0.0,
"stake_amount": pytest.approx(60),
"is_final_exit": False,
"final_profit_ratio": None,
"final_profit_ratio": ANY,
} == last_msg
+3 -3
View File
@@ -941,7 +941,7 @@ def test_backtest_one_detail(default_conf_usdt, mocker, testdatadir, use_detail)
"use_detail,exp_funding_fee, exp_ff_updates",
[
(True, -0.018054162, 11),
(False, -0.01780296, 5),
(False, -0.01780296, 6),
],
)
def test_backtest_one_detail_futures(
@@ -1051,8 +1051,8 @@ def test_backtest_one_detail_futures(
@pytest.mark.parametrize(
"use_detail,entries,max_stake,ff_updates,expected_ff",
[
(True, 50, 3000, 54, -1.18038144),
(False, 6, 360, 10, -0.14679994),
(True, 50, 3000, 55, -1.18038144),
(False, 6, 360, 11, -0.14679994),
],
)
def test_backtest_one_detail_futures_funding_fees(
+105
View File
@@ -13,6 +13,12 @@ from freqtrade.optimize.analysis.lookahead_helpers import LookaheadAnalysisSubFu
from tests.conftest import EXMS, get_args, log_has_re, patch_exchange
IGNORE_BIASED_INDICATORS_CAPTION = (
"Any indicators in 'biased_indicators' which are used within "
"set_freqai_targets() can be ignored."
)
@pytest.fixture
def lookahead_conf(default_conf_usdt, tmp_path):
default_conf_usdt["user_data_dir"] = tmp_path
@@ -133,6 +139,58 @@ def test_lookahead_helper_start(lookahead_conf, mocker) -> None:
text_table_mock.reset_mock()
@pytest.mark.parametrize(
"indicators, expected_caption_text",
[
(
["&indicator1", "indicator2"],
IGNORE_BIASED_INDICATORS_CAPTION,
),
(
["indicator1", "&indicator2"],
IGNORE_BIASED_INDICATORS_CAPTION,
),
(
["&indicator1", "&indicator2"],
IGNORE_BIASED_INDICATORS_CAPTION,
),
(["indicator1", "indicator2"], None),
([], None),
],
ids=(
"First of two biased indicators starts with '&'",
"Second of two biased indicators starts with '&'",
"Both biased indicators start with '&'",
"No biased indicators start with '&'",
"Empty biased indicators list",
),
)
def test_lookahead_helper_start__caption_based_on_indicators(
indicators, expected_caption_text, lookahead_conf, mocker
):
"""Test that the table caption is only populated if a biased_indicator starts with '&'."""
single_mock = MagicMock()
lookahead_analysis = LookaheadAnalysis(
lookahead_conf,
{"name": "strategy_test_v3_with_lookahead_bias"},
)
lookahead_analysis.current_analysis.false_indicators = indicators
single_mock.return_value = lookahead_analysis
text_table_mock = MagicMock()
mocker.patch.multiple(
"freqtrade.optimize.analysis.lookahead_helpers.LookaheadAnalysisSubFunctions",
initialize_single_lookahead_analysis=single_mock,
text_table_lookahead_analysis_instances=text_table_mock,
)
LookaheadAnalysisSubFunctions.start(lookahead_conf)
text_table_mock.assert_called_once_with(
lookahead_conf, [lookahead_analysis], caption=expected_caption_text
)
def test_lookahead_helper_text_table_lookahead_analysis_instances(lookahead_conf):
analysis = Analysis()
analysis.has_bias = True
@@ -199,6 +257,53 @@ def test_lookahead_helper_text_table_lookahead_analysis_instances(lookahead_conf
assert len(data) == 3
@pytest.mark.parametrize(
"caption",
[
"",
"A test caption",
None,
False,
],
ids=(
"Pass empty string",
"Pass non-empty string",
"Pass None",
"Don't pass caption",
),
)
def test_lookahead_helper_text_table_lookahead_analysis_instances__caption(
caption,
lookahead_conf,
mocker,
):
"""Test that the caption is passed in the table kwargs when calling print_rich_table()."""
print_rich_table_mock = MagicMock()
mocker.patch(
"freqtrade.optimize.analysis.lookahead_helpers.print_rich_table",
print_rich_table_mock,
)
lookahead_analysis = LookaheadAnalysis(
lookahead_conf,
{
"name": "strategy_test_v3_with_lookahead_bias",
"location": Path(lookahead_conf["strategy_path"], f"{lookahead_conf['strategy']}.py"),
},
)
kwargs = {}
if caption is not False:
kwargs["caption"] = caption
LookaheadAnalysisSubFunctions.text_table_lookahead_analysis_instances(
lookahead_conf, [lookahead_analysis], **kwargs
)
assert print_rich_table_mock.call_args[-1]["table_kwargs"]["caption"] == (
caption if caption is not False else None
)
def test_lookahead_helper_export_to_csv(lookahead_conf):
import pandas as pd
+11 -6
View File
@@ -293,20 +293,25 @@ def test_store_backtest_candles(testdatadir, mocker):
candle_dict = {"DefStrat": {"UNITTEST/BTC": pd.DataFrame()}}
# mock directory exporting
store_backtest_analysis_results(testdatadir, candle_dict, {}, "2022_01_01_15_05_13")
store_backtest_analysis_results(testdatadir, candle_dict, {}, {}, "2022_01_01_15_05_13")
assert dump_mock.call_count == 2
assert dump_mock.call_count == 3
assert isinstance(dump_mock.call_args_list[0][0][0], Path)
assert str(dump_mock.call_args_list[0][0][0]).endswith("_signals.pkl")
assert str(dump_mock.call_args_list[1][0][0]).endswith("_rejected.pkl")
assert str(dump_mock.call_args_list[2][0][0]).endswith("_exited.pkl")
dump_mock.reset_mock()
# mock file exporting
filename = Path(testdatadir / "testresult")
store_backtest_analysis_results(filename, candle_dict, {}, "2022_01_01_15_05_13")
assert dump_mock.call_count == 2
store_backtest_analysis_results(filename, candle_dict, {}, {}, "2022_01_01_15_05_13")
assert dump_mock.call_count == 3
assert isinstance(dump_mock.call_args_list[0][0][0], Path)
# result will be testdatadir / testresult-<timestamp>_signals.pkl
assert str(dump_mock.call_args_list[0][0][0]).endswith("_signals.pkl")
assert str(dump_mock.call_args_list[1][0][0]).endswith("_rejected.pkl")
assert str(dump_mock.call_args_list[2][0][0]).endswith("_exited.pkl")
dump_mock.reset_mock()
@@ -315,7 +320,7 @@ def test_write_read_backtest_candles(tmp_path):
# test directory exporting
sample_date = "2022_01_01_15_05_13"
store_backtest_analysis_results(tmp_path, candle_dict, {}, sample_date)
store_backtest_analysis_results(tmp_path, candle_dict, {}, {}, sample_date)
stored_file = tmp_path / f"backtest-result-{sample_date}_signals.pkl"
with stored_file.open("rb") as scp:
pickled_signal_candles = joblib.load(scp)
@@ -330,7 +335,7 @@ def test_write_read_backtest_candles(tmp_path):
# test file exporting
filename = tmp_path / "testresult"
store_backtest_analysis_results(filename, candle_dict, {}, sample_date)
store_backtest_analysis_results(filename, candle_dict, {}, {}, sample_date)
stored_file = tmp_path / f"testresult-{sample_date}_signals.pkl"
with stored_file.open("rb") as scp:
pickled_signal_candles = joblib.load(scp)
+7 -4
View File
@@ -149,7 +149,10 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
# Different from "filled" response:
response_unfilled.update(
{
"amount": 91.07468124,
"amount": 0.0,
"open_trade_value": 0.0,
"stoploss_entry_dist": 0.0,
"stoploss_entry_dist_ratio": 0.0,
"profit_ratio": 0.0,
"profit_pct": 0.0,
"profit_abs": 0.0,
@@ -762,7 +765,7 @@ def test_rpc_force_exit(default_conf, ticker, fee, mocker) -> None:
freqtradebot.enter_positions()
# make an limit-buy open trade
trade = Trade.session.scalars(select(Trade).filter(Trade.id == "3")).first()
filled_amount = trade.amount / 2
filled_amount = trade.amount_requested / 2
# Fetch order - it's open first, and closed after cancel_order is called.
mocker.patch(
f"{EXMS}.fetch_order",
@@ -799,7 +802,7 @@ def test_rpc_force_exit(default_conf, ticker, fee, mocker) -> None:
cancel_order_mock.reset_mock()
trade = Trade.session.scalars(select(Trade).filter(Trade.id == "3")).first()
amount = trade.amount
amount = trade.amount_requested
# make an limit-sell open order trade
mocker.patch(
f"{EXMS}.fetch_order",
@@ -832,7 +835,7 @@ def test_rpc_force_exit(default_conf, ticker, fee, mocker) -> None:
assert cancel_order_mock.call_count == 0
trade = Trade.session.scalars(select(Trade).filter(Trade.id == "4")).first()
amount = trade.amount
amount = trade.amount_requested
# make an limit-buy open trade, if there is no 'filled', don't sell it
mocker.patch(
f"{EXMS}.fetch_order",
+7 -2
View File
@@ -365,13 +365,18 @@ def test_sync_wallet_dry(mocker, default_conf_usdt, fee):
assert bal["NEO"].total == 10
assert bal["XRP"].total == 10
assert bal["LTC"].total == 2
assert bal["USDT"].total == 922.74
usdt_bal = bal["USDT"]
assert usdt_bal.free == 922.74
assert usdt_bal.total == 942.74
assert usdt_bal.used == 20.0
# sum of used and free should be total.
assert usdt_bal.total == usdt_bal.free + usdt_bal.used
assert freqtrade.wallets.get_starting_balance() == default_conf_usdt["dry_run_wallet"]
total = freqtrade.wallets.get_total("LTC")
free = freqtrade.wallets.get_free("LTC")
used = freqtrade.wallets.get_used("LTC")
assert free != 0
assert used != 0
assert free + used == total