mirror of
https://github.com/freqtrade/freqtrade.git
synced 2026-05-02 15:09:09 +00:00
Merge branch 'develop' into fix_merge_informative_pair
This commit is contained in:
@@ -658,7 +658,9 @@ def test_start_new_strategy_no_arg():
|
||||
args = [
|
||||
"new-strategy",
|
||||
]
|
||||
with pytest.raises(OperationalException, match="`new-strategy` requires --strategy to be set."):
|
||||
with pytest.raises(
|
||||
OperationalException, match=r"`new-strategy` requires --strategy to be set\."
|
||||
):
|
||||
start_new_strategy(get_args(args))
|
||||
|
||||
|
||||
@@ -803,7 +805,7 @@ def test_get_ui_download_url_direct(mocker):
|
||||
assert last_version == "0.0.1"
|
||||
assert x == "http://download1.zip"
|
||||
|
||||
with pytest.raises(ValueError, match="UI-Version not found."):
|
||||
with pytest.raises(ValueError, match=r"UI-Version not found\."):
|
||||
x, last_version = get_ui_download_url("0.0.3", False)
|
||||
|
||||
|
||||
@@ -1650,7 +1652,7 @@ def test_hyperopt_show(mocker, capsys):
|
||||
pargs = get_args(args)
|
||||
pargs["config"] = None
|
||||
with pytest.raises(
|
||||
OperationalException, match="The index of the epoch to show should be greater than -4."
|
||||
OperationalException, match=r"The index of the epoch to show should be greater than -4\."
|
||||
):
|
||||
start_hyperopt_show(pargs)
|
||||
|
||||
@@ -1658,7 +1660,7 @@ def test_hyperopt_show(mocker, capsys):
|
||||
pargs = get_args(args)
|
||||
pargs["config"] = None
|
||||
with pytest.raises(
|
||||
OperationalException, match="The index of the epoch to show should be less than 4."
|
||||
OperationalException, match=r"The index of the epoch to show should be less than 4\."
|
||||
):
|
||||
start_hyperopt_show(pargs)
|
||||
|
||||
@@ -2032,5 +2034,7 @@ def test_start_edge():
|
||||
]
|
||||
|
||||
pargs = get_args(args)
|
||||
with pytest.raises(OperationalException, match="The Edge module has been deprecated in 2023.9"):
|
||||
with pytest.raises(
|
||||
OperationalException, match=r"The Edge module has been deprecated in 2023\.9"
|
||||
):
|
||||
start_edge(pargs)
|
||||
|
||||
@@ -75,7 +75,7 @@ def test_get_latest_hyperopt_file(testdatadir):
|
||||
# Test with absolute path
|
||||
with pytest.raises(
|
||||
OperationalException,
|
||||
match="--hyperopt-filename expects only the filename, not an absolute path.",
|
||||
match=r"--hyperopt-filename expects only the filename, not an absolute path\.",
|
||||
):
|
||||
get_latest_hyperopt_file(str(testdatadir.parent), str(testdatadir.parent))
|
||||
|
||||
@@ -344,7 +344,7 @@ def test_create_cum_profit1(testdatadir):
|
||||
assert cum_profits.iloc[0]["cum_profits"] == 0
|
||||
assert pytest.approx(cum_profits.iloc[-1]["cum_profits"]) == 9.0225563e-05
|
||||
|
||||
with pytest.raises(ValueError, match="Trade dataframe empty."):
|
||||
with pytest.raises(ValueError, match=r"Trade dataframe empty\."):
|
||||
create_cum_profit(
|
||||
df.set_index("date"),
|
||||
bt_data[bt_data["pair"] == "NOTAPAIR"],
|
||||
@@ -369,10 +369,10 @@ def test_calculate_max_drawdown(testdatadir):
|
||||
underwater = calculate_underwater(bt_data)
|
||||
assert isinstance(underwater, DataFrame)
|
||||
|
||||
with pytest.raises(ValueError, match="Trade dataframe empty."):
|
||||
with pytest.raises(ValueError, match=r"Trade dataframe empty\."):
|
||||
calculate_max_drawdown(DataFrame())
|
||||
|
||||
with pytest.raises(ValueError, match="Trade dataframe empty."):
|
||||
with pytest.raises(ValueError, match=r"Trade dataframe empty\."):
|
||||
calculate_underwater(DataFrame())
|
||||
|
||||
|
||||
@@ -391,7 +391,7 @@ def test_calculate_csum(testdatadir):
|
||||
assert csum_min1 == csum_min + 5
|
||||
assert csum_max1 == csum_max + 5
|
||||
|
||||
with pytest.raises(ValueError, match="Trade dataframe empty."):
|
||||
with pytest.raises(ValueError, match=r"Trade dataframe empty\."):
|
||||
csum_min, csum_max = calculate_csum(DataFrame())
|
||||
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ from freqtrade.data.converter import (
|
||||
convert_trades_to_ohlcv,
|
||||
ohlcv_fill_up_missing_data,
|
||||
ohlcv_to_dataframe,
|
||||
order_book_to_dataframe,
|
||||
reduce_dataframe_footprint,
|
||||
trades_df_remove_duplicates,
|
||||
trades_dict_to_list,
|
||||
@@ -49,7 +50,7 @@ def test_ohlcv_to_dataframe(ohlcv_history_list, caplog):
|
||||
|
||||
def test_trades_to_ohlcv(trades_history_df, caplog):
|
||||
caplog.set_level(logging.DEBUG)
|
||||
with pytest.raises(ValueError, match="Trade-list empty."):
|
||||
with pytest.raises(ValueError, match=r"Trade-list empty\."):
|
||||
trades_to_ohlcv(pd.DataFrame(columns=trades_history_df.columns), "1m")
|
||||
|
||||
df = trades_to_ohlcv(trades_history_df, "1m")
|
||||
@@ -588,3 +589,77 @@ def test_convert_trades_to_ohlcv(testdatadir, tmp_path, caplog):
|
||||
candle_type=CandleType.SPOT,
|
||||
)
|
||||
assert log_has(msg, caplog)
|
||||
|
||||
|
||||
def test_order_book_to_dataframe():
|
||||
bids = [
|
||||
[100.0, 5.0],
|
||||
[99.5, 3.0],
|
||||
[99.0, 2.0],
|
||||
]
|
||||
asks = [
|
||||
[100.5, 4.0],
|
||||
[101.0, 6.0],
|
||||
[101.5, 1.0],
|
||||
]
|
||||
|
||||
result = order_book_to_dataframe(bids, asks)
|
||||
|
||||
assert isinstance(result, pd.DataFrame)
|
||||
|
||||
expected_columns = ["b_sum", "b_size", "bids", "asks", "a_size", "a_sum"]
|
||||
assert result.columns.tolist() == expected_columns
|
||||
|
||||
assert len(result) == max(len(bids), len(asks))
|
||||
|
||||
assert result["bids"].tolist() == [100.0, 99.5, 99.0]
|
||||
assert result["b_size"].tolist() == [5.0, 3.0, 2.0]
|
||||
assert result["b_sum"].tolist() == [5.0, 8.0, 10.0]
|
||||
|
||||
assert result["asks"].tolist() == [100.5, 101.0, 101.5]
|
||||
assert result["a_size"].tolist() == [4.0, 6.0, 1.0]
|
||||
assert result["a_sum"].tolist() == [4.0, 10.0, 11.0]
|
||||
|
||||
|
||||
def test_order_book_to_dataframe_empty():
|
||||
bids = []
|
||||
asks = []
|
||||
|
||||
result = order_book_to_dataframe(bids, asks)
|
||||
|
||||
assert isinstance(result, pd.DataFrame)
|
||||
|
||||
expected_columns = ["b_sum", "b_size", "bids", "asks", "a_size", "a_sum"]
|
||||
assert result.columns.tolist() == expected_columns
|
||||
# Empty input should result in empty dataframe
|
||||
assert len(result) == 0
|
||||
|
||||
|
||||
def test_order_book_to_dataframe_unequal_lengths():
|
||||
bids = [
|
||||
[100.0, 5.0],
|
||||
[99.5, 3.0],
|
||||
[99.0, 2.0],
|
||||
[98.5, 1.0],
|
||||
]
|
||||
asks = [
|
||||
[100.5, 4.0],
|
||||
[101.0, 6.0],
|
||||
]
|
||||
|
||||
result = order_book_to_dataframe(bids, asks)
|
||||
|
||||
assert len(result) == max(len(bids), len(asks))
|
||||
assert len(result) == 4
|
||||
|
||||
assert result["bids"].tolist() == [100.0, 99.5, 99.0, 98.5]
|
||||
assert result["b_size"].tolist() == [5.0, 3.0, 2.0, 1.0]
|
||||
assert result["b_sum"].tolist() == [5.0, 8.0, 10.0, 11.0]
|
||||
|
||||
assert result["asks"].tolist()[:2] == [100.5, 101.0]
|
||||
# NA for missing asks
|
||||
assert pd.isna(result["asks"].iloc[2])
|
||||
assert pd.isna(result["asks"].iloc[3])
|
||||
|
||||
assert result["a_size"].tolist()[:2] == [4.0, 6.0]
|
||||
assert result["a_sum"].tolist()[:2] == [4.0, 10.0]
|
||||
|
||||
@@ -8,6 +8,7 @@ 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 freqtrade.util import dt_utc
|
||||
from tests.conftest import EXMS, generate_test_data, get_patched_exchange
|
||||
|
||||
|
||||
@@ -449,6 +450,12 @@ def test_no_exchange_mode(default_conf):
|
||||
with pytest.raises(OperationalException, match=message):
|
||||
dp.available_pairs()
|
||||
|
||||
with pytest.raises(OperationalException, match=message):
|
||||
dp.funding_rate("XRP/USDT:USDT")
|
||||
|
||||
with pytest.raises(OperationalException, match=message):
|
||||
dp.check_delisting("XRP/USDT")
|
||||
|
||||
|
||||
def test_dp_send_msg(default_conf):
|
||||
default_conf["runmode"] = RunMode.DRY_RUN
|
||||
@@ -612,3 +619,20 @@ def test_dp_get_required_startup(default_conf_usdt):
|
||||
assert dp.get_required_startup("5m") == 51880
|
||||
assert dp.get_required_startup("1h") == 4360
|
||||
assert dp.get_required_startup("1d") == 220
|
||||
|
||||
|
||||
def test_check_delisting(mocker, default_conf_usdt):
|
||||
delist_mock = MagicMock(return_value=None)
|
||||
exchange = get_patched_exchange(mocker, default_conf_usdt)
|
||||
mocker.patch.object(exchange, "check_delisting_time", delist_mock)
|
||||
dp = DataProvider(default_conf_usdt, exchange)
|
||||
res = dp.check_delisting("ETH/USDT")
|
||||
assert res is None
|
||||
assert delist_mock.call_count == 1
|
||||
|
||||
delist_mock2 = MagicMock(return_value=dt_utc(2025, 10, 2))
|
||||
mocker.patch.object(exchange, "check_delisting_time", delist_mock2)
|
||||
res = dp.check_delisting("XRP/USDT")
|
||||
assert res == dt_utc(2025, 10, 2)
|
||||
|
||||
assert delist_mock2.call_count == 1
|
||||
|
||||
@@ -18,6 +18,7 @@ from freqtrade.data.converter import ohlcv_to_dataframe
|
||||
from freqtrade.data.history import get_datahandler
|
||||
from freqtrade.data.history.datahandlers.jsondatahandler import JsonDataHandler, JsonGzDataHandler
|
||||
from freqtrade.data.history.history_utils import (
|
||||
_download_all_pairs_history_parallel,
|
||||
_download_pair_history,
|
||||
_download_trades_history,
|
||||
_load_cached_data_for_updating,
|
||||
@@ -545,6 +546,14 @@ def test_refresh_backtest_ohlcv_data(
|
||||
):
|
||||
caplog.set_level(logging.DEBUG)
|
||||
dl_mock = mocker.patch("freqtrade.data.history.history_utils._download_pair_history")
|
||||
|
||||
def parallel_mock(pairs, timeframe, candle_type, **kwargs):
|
||||
return {(pair, timeframe, candle_type): DataFrame() for pair in pairs}
|
||||
|
||||
parallel_mock = mocker.patch(
|
||||
"freqtrade.data.history.history_utils._download_all_pairs_history_parallel",
|
||||
side_effect=parallel_mock,
|
||||
)
|
||||
mocker.patch(f"{EXMS}.markets", PropertyMock(return_value=markets))
|
||||
|
||||
mocker.patch.object(Path, "exists", MagicMock(return_value=True))
|
||||
@@ -559,10 +568,12 @@ def test_refresh_backtest_ohlcv_data(
|
||||
timeframes=["1m", "5m"],
|
||||
datadir=testdatadir,
|
||||
timerange=timerange,
|
||||
erase=True,
|
||||
erase=False,
|
||||
trading_mode=trademode,
|
||||
)
|
||||
|
||||
# Called once per timeframe (as we return an empty dataframe)
|
||||
assert parallel_mock.call_count == 2
|
||||
assert dl_mock.call_count == callcount
|
||||
assert dl_mock.call_args[1]["timerange"].starttype == "date"
|
||||
|
||||
@@ -699,3 +710,256 @@ def test_download_trades_history(
|
||||
assert ght_mock.call_count == 0
|
||||
|
||||
_clean_test_file(file2)
|
||||
|
||||
|
||||
def test_download_all_pairs_history_parallel(mocker, default_conf_usdt):
|
||||
pairs = ["PAIR1/BTC", "PAIR2/USDT"]
|
||||
timeframe = "5m"
|
||||
candle_type = CandleType.SPOT
|
||||
|
||||
df1 = DataFrame(
|
||||
{
|
||||
"date": [1, 2],
|
||||
"open": [1, 2],
|
||||
"close": [1, 2],
|
||||
"high": [1, 2],
|
||||
"low": [1, 2],
|
||||
"volume": [1, 2],
|
||||
}
|
||||
)
|
||||
df2 = DataFrame(
|
||||
{
|
||||
"date": [3, 4],
|
||||
"open": [3, 4],
|
||||
"close": [3, 4],
|
||||
"high": [3, 4],
|
||||
"low": [3, 4],
|
||||
"volume": [3, 4],
|
||||
}
|
||||
)
|
||||
expected = {
|
||||
("PAIR1/BTC", timeframe, candle_type): df1,
|
||||
("PAIR2/USDT", timeframe, candle_type): df2,
|
||||
}
|
||||
# Mock exchange
|
||||
mocker.patch.multiple(
|
||||
EXMS,
|
||||
exchange_has=MagicMock(return_value=True),
|
||||
ohlcv_candle_limit=MagicMock(return_value=1000),
|
||||
refresh_latest_ohlcv=MagicMock(return_value=expected),
|
||||
)
|
||||
exchange = get_patched_exchange(mocker, default_conf_usdt)
|
||||
# timerange with starttype 'date' and startts far in the future to trigger parallel download
|
||||
|
||||
timerange = TimeRange("date", None, 9999999999, 0)
|
||||
result = _download_all_pairs_history_parallel(
|
||||
exchange=exchange,
|
||||
pairs=pairs,
|
||||
timeframe=timeframe,
|
||||
candle_type=candle_type,
|
||||
timerange=timerange,
|
||||
)
|
||||
assert result == expected
|
||||
|
||||
assert exchange.ohlcv_candle_limit.call_args[0] == (timeframe, candle_type)
|
||||
assert exchange.refresh_latest_ohlcv.call_count == 1
|
||||
|
||||
# If since is not after one_call_min_time_dt, should not call refresh_latest_ohlcv
|
||||
exchange.refresh_latest_ohlcv.reset_mock()
|
||||
timerange2 = TimeRange("date", None, 0, 0)
|
||||
result2 = _download_all_pairs_history_parallel(
|
||||
exchange=exchange,
|
||||
pairs=pairs,
|
||||
timeframe=timeframe,
|
||||
candle_type=candle_type,
|
||||
timerange=timerange2,
|
||||
)
|
||||
assert result2 == {}
|
||||
assert exchange.refresh_latest_ohlcv.call_count == 0
|
||||
|
||||
exchange.refresh_latest_ohlcv.reset_mock()
|
||||
|
||||
# Test without timerange
|
||||
result3 = _download_all_pairs_history_parallel(
|
||||
exchange=exchange,
|
||||
pairs=pairs,
|
||||
timeframe=timeframe,
|
||||
candle_type=candle_type,
|
||||
timerange=None,
|
||||
)
|
||||
assert result3 == {}
|
||||
assert exchange.refresh_latest_ohlcv.call_count == 0
|
||||
|
||||
|
||||
def test_download_pair_history_with_pair_candles(mocker, default_conf, tmp_path, caplog) -> None:
|
||||
"""
|
||||
Test _download_pair_history with pair_candles parameter (parallel method).
|
||||
"""
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
|
||||
# Create test data for existing cached data
|
||||
existing_data = DataFrame(
|
||||
{
|
||||
"date": [dt_utc(2018, 1, 10, 10, 0), dt_utc(2018, 1, 10, 10, 5)],
|
||||
"open": [1.0, 1.15],
|
||||
"high": [1.1, 1.2],
|
||||
"low": [0.9, 1.1],
|
||||
"close": [1.05, 1.15],
|
||||
"volume": [100, 150],
|
||||
}
|
||||
)
|
||||
|
||||
# Create pair_candles data that will be used instead of exchange download
|
||||
# This data should start before or at the same time as since_ms to trigger the else branch
|
||||
pair_candles_data = DataFrame(
|
||||
{
|
||||
"date": [
|
||||
dt_utc(2018, 1, 10, 10, 5),
|
||||
dt_utc(2018, 1, 10, 10, 10),
|
||||
dt_utc(2018, 1, 10, 10, 15),
|
||||
],
|
||||
"open": [1.15, 1.2, 1.25],
|
||||
"high": [1.25, 1.3, 1.35],
|
||||
"low": [1.1, 1.15, 1.2],
|
||||
"close": [1.2, 1.25, 1.3],
|
||||
"volume": [200, 250, 300],
|
||||
}
|
||||
)
|
||||
|
||||
# Mock the data handler to return existing cached data
|
||||
data_handler_mock = MagicMock()
|
||||
data_handler_mock.ohlcv_load.return_value = existing_data
|
||||
data_handler_mock.ohlcv_store = MagicMock()
|
||||
mocker.patch(
|
||||
"freqtrade.data.history.history_utils.get_datahandler", return_value=data_handler_mock
|
||||
)
|
||||
|
||||
# Mock _load_cached_data_for_updating to return existing data and since_ms
|
||||
since_ms = dt_ts(dt_utc(2018, 1, 10, 10, 5)) # Time of last existing candle
|
||||
mocker.patch(
|
||||
"freqtrade.data.history.history_utils._load_cached_data_for_updating",
|
||||
return_value=(existing_data, since_ms, None),
|
||||
)
|
||||
|
||||
# Mock clean_ohlcv_dataframe to return concatenated data
|
||||
expected_result = DataFrame(
|
||||
{
|
||||
"date": [
|
||||
dt_utc(2018, 1, 10, 10, 0),
|
||||
dt_utc(2018, 1, 10, 10, 5),
|
||||
dt_utc(2018, 1, 10, 10, 10),
|
||||
dt_utc(2018, 1, 10, 10, 15),
|
||||
],
|
||||
"open": [1.0, 1.15, 1.2, 1.25],
|
||||
"high": [1.1, 1.25, 1.3, 1.35],
|
||||
"low": [0.9, 1.1, 1.15, 1.2],
|
||||
"close": [1.05, 1.2, 1.25, 1.3],
|
||||
"volume": [100, 200, 250, 300],
|
||||
}
|
||||
)
|
||||
|
||||
get_historic_ohlcv_mock = MagicMock()
|
||||
mocker.patch.object(exchange, "get_historic_ohlcv", get_historic_ohlcv_mock)
|
||||
|
||||
# Call _download_pair_history with pre-loaded pair_candles
|
||||
result = _download_pair_history(
|
||||
datadir=tmp_path,
|
||||
exchange=exchange,
|
||||
pair="TEST/BTC",
|
||||
timeframe="5m",
|
||||
candle_type=CandleType.SPOT,
|
||||
pair_candles=pair_candles_data,
|
||||
)
|
||||
|
||||
# Verify the function succeeded
|
||||
assert result is True
|
||||
|
||||
# Verify that exchange.get_historic_ohlcv was NOT called (parallel method was used)
|
||||
assert get_historic_ohlcv_mock.call_count == 0
|
||||
|
||||
# Verify the log message indicating parallel method was used (line 315-316)
|
||||
assert log_has("Downloaded data for TEST/BTC with length 3. Parallel Method.", caplog)
|
||||
|
||||
# Verify data was stored
|
||||
assert data_handler_mock.ohlcv_store.call_count == 1
|
||||
stored_data = data_handler_mock.ohlcv_store.call_args_list[0][1]["data"]
|
||||
assert stored_data.equals(expected_result)
|
||||
assert len(stored_data) == 4
|
||||
|
||||
|
||||
def test_download_pair_history_with_pair_candles_no_overlap(
|
||||
mocker, default_conf, tmp_path, caplog
|
||||
) -> None:
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
|
||||
# Create test data for existing cached data
|
||||
existing_data = DataFrame(
|
||||
{
|
||||
"date": [dt_utc(2018, 1, 10, 10, 0), dt_utc(2018, 1, 10, 10, 5)],
|
||||
"open": [1.0, 1.1],
|
||||
"high": [1.1, 1.2],
|
||||
"low": [0.9, 1.0],
|
||||
"close": [1.05, 1.15],
|
||||
"volume": [100, 150],
|
||||
}
|
||||
)
|
||||
|
||||
# Create pair_candles data that will be used instead of exchange download
|
||||
# This data should start before or at the same time as since_ms to trigger the else branch
|
||||
pair_candles_data = DataFrame(
|
||||
{
|
||||
"date": [
|
||||
dt_utc(2018, 1, 10, 10, 10),
|
||||
dt_utc(2018, 1, 10, 10, 15),
|
||||
dt_utc(2018, 1, 10, 10, 20),
|
||||
],
|
||||
"open": [1.15, 1.2, 1.25],
|
||||
"high": [1.25, 1.3, 1.35],
|
||||
"low": [1.1, 1.15, 1.2],
|
||||
"close": [1.2, 1.25, 1.3],
|
||||
"volume": [200, 250, 300],
|
||||
}
|
||||
)
|
||||
|
||||
# Mock the data handler to return existing cached data
|
||||
data_handler_mock = MagicMock()
|
||||
data_handler_mock.ohlcv_load.return_value = existing_data
|
||||
data_handler_mock.ohlcv_store = MagicMock()
|
||||
mocker.patch(
|
||||
"freqtrade.data.history.history_utils.get_datahandler", return_value=data_handler_mock
|
||||
)
|
||||
|
||||
# Mock _load_cached_data_for_updating to return existing data and since_ms
|
||||
since_ms = dt_ts(dt_utc(2018, 1, 10, 10, 5)) # Time of last existing candle
|
||||
mocker.patch(
|
||||
"freqtrade.data.history.history_utils._load_cached_data_for_updating",
|
||||
return_value=(existing_data, since_ms, None),
|
||||
)
|
||||
|
||||
get_historic_ohlcv_mock = MagicMock(return_value=DataFrame())
|
||||
mocker.patch.object(exchange, "get_historic_ohlcv", get_historic_ohlcv_mock)
|
||||
|
||||
# Call _download_pair_history with pre-loaded pair_candles
|
||||
result = _download_pair_history(
|
||||
datadir=tmp_path,
|
||||
exchange=exchange,
|
||||
pair="TEST/BTC",
|
||||
timeframe="5m",
|
||||
candle_type=CandleType.SPOT,
|
||||
pair_candles=pair_candles_data,
|
||||
)
|
||||
|
||||
# Verify the function succeeded
|
||||
assert result is True
|
||||
|
||||
# Verify that exchange.get_historic_ohlcv was NOT called (parallel method was used)
|
||||
assert get_historic_ohlcv_mock.call_count == 1
|
||||
|
||||
# Verify the log message indicating parallel method was used (line 315-316)
|
||||
assert not log_has_re(r"Downloaded .* Parallel Method.", caplog)
|
||||
|
||||
# Verify data was stored
|
||||
assert data_handler_mock.ohlcv_store.call_count == 1
|
||||
stored_data = data_handler_mock.ohlcv_store.call_args_list[0][1]["data"]
|
||||
assert stored_data.equals(existing_data)
|
||||
assert len(stored_data) == 2
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from copy import deepcopy
|
||||
from datetime import datetime, timedelta
|
||||
from random import randint
|
||||
from unittest.mock import MagicMock, PropertyMock
|
||||
@@ -7,7 +8,7 @@ import pandas as pd
|
||||
import pytest
|
||||
|
||||
from freqtrade.data.converter.trade_converter import trades_dict_to_list
|
||||
from freqtrade.enums import CandleType, MarginMode, TradingMode
|
||||
from freqtrade.enums import CandleType, MarginMode, RunMode, TradingMode
|
||||
from freqtrade.exceptions import DependencyException, InvalidOrderException, OperationalException
|
||||
from freqtrade.exchange.exchange_utils_timeframe import timeframe_to_seconds
|
||||
from freqtrade.persistence import Trade
|
||||
@@ -1108,3 +1109,84 @@ async def test__async_get_trade_history_id_binance_fast(
|
||||
|
||||
# Clean up event loop to avoid warnings
|
||||
exchange.close()
|
||||
|
||||
|
||||
def test_check_delisting_time_binance(default_conf_usdt, mocker):
|
||||
exchange = get_patched_exchange(mocker, default_conf_usdt, exchange="binance")
|
||||
exchange._config["runmode"] = RunMode.BACKTEST
|
||||
delist_mock = MagicMock(return_value=None)
|
||||
delist_fut_mock = MagicMock(return_value=None)
|
||||
mocker.patch.object(exchange, "_get_spot_pair_delist_time", delist_mock)
|
||||
mocker.patch.object(exchange, "_check_delisting_futures", delist_fut_mock)
|
||||
|
||||
# Invalid run mode
|
||||
resp = exchange.check_delisting_time("BTC/USDT")
|
||||
assert resp is None
|
||||
assert delist_mock.call_count == 0
|
||||
assert delist_fut_mock.call_count == 0
|
||||
|
||||
# Delist spot called
|
||||
exchange._config["runmode"] = RunMode.DRY_RUN
|
||||
resp1 = exchange.check_delisting_time("BTC/USDT")
|
||||
assert resp1 is None
|
||||
assert delist_mock.call_count == 1
|
||||
assert delist_fut_mock.call_count == 0
|
||||
delist_mock.reset_mock()
|
||||
|
||||
# Delist futures called
|
||||
exchange.trading_mode = TradingMode.FUTURES
|
||||
resp1 = exchange.check_delisting_time("BTC/USDT:USDT")
|
||||
assert resp1 is None
|
||||
assert delist_mock.call_count == 0
|
||||
assert delist_fut_mock.call_count == 1
|
||||
|
||||
|
||||
def test__check_delisting_futures_binance(default_conf_usdt, mocker, markets):
|
||||
markets["BTC/USDT:USDT"] = deepcopy(markets["SOL/BUSD:BUSD"])
|
||||
markets["BTC/USDT:USDT"]["info"]["deliveryDate"] = 4133404800000
|
||||
markets["SOL/BUSD:BUSD"]["info"]["deliveryDate"] = 4133404800000
|
||||
markets["ADA/USDT:USDT"]["info"]["deliveryDate"] = 1760745600000 # 2025-10-18
|
||||
exchange = get_patched_exchange(mocker, default_conf_usdt, exchange="binance")
|
||||
mocker.patch(f"{EXMS}.markets", PropertyMock(return_value=markets))
|
||||
|
||||
resp_sol = exchange._check_delisting_futures("SOL/BUSD:BUSD")
|
||||
# Delisting is equal to BTC
|
||||
assert resp_sol is None
|
||||
# Actually has a delisting date
|
||||
resp_ada = exchange._check_delisting_futures("ADA/USDT:USDT")
|
||||
assert resp_ada == dt_utc(2025, 10, 18)
|
||||
|
||||
|
||||
def test__get_spot_delist_schedule_binance(default_conf_usdt, mocker):
|
||||
exchange = get_patched_exchange(mocker, default_conf_usdt, exchange="binance")
|
||||
ret_value = [{"delistTime": 1759114800000, "symbols": ["ETCBTC"]}]
|
||||
schedule_mock = mocker.patch.object(exchange, "_get_spot_delist_schedule", return_value=None)
|
||||
|
||||
# None - mode is DRY
|
||||
assert exchange._get_spot_pair_delist_time("ETC/BTC") is None
|
||||
# Switch to live
|
||||
exchange._config["runmode"] = RunMode.LIVE
|
||||
assert exchange._get_spot_pair_delist_time("ETC/BTC") is None
|
||||
|
||||
mocker.patch.object(exchange, "_get_spot_delist_schedule", return_value=ret_value)
|
||||
resp = exchange._get_spot_pair_delist_time("ETC/BTC")
|
||||
assert resp == dt_utc(2025, 9, 29, 3, 0)
|
||||
assert schedule_mock.call_count == 1
|
||||
schedule_mock.reset_mock()
|
||||
|
||||
# Caching - don't refresh.
|
||||
assert exchange._get_spot_pair_delist_time("ETC/BTC", refresh=False) == dt_utc(
|
||||
2025, 9, 29, 3, 0
|
||||
)
|
||||
assert schedule_mock.call_count == 0
|
||||
|
||||
api_mock = MagicMock()
|
||||
ccxt_exceptionhandlers(
|
||||
mocker,
|
||||
default_conf_usdt,
|
||||
api_mock,
|
||||
"binance",
|
||||
"_get_spot_delist_schedule",
|
||||
"sapi_get_spot_delist_schedule",
|
||||
retries=1,
|
||||
)
|
||||
|
||||
@@ -28,6 +28,7 @@ from freqtrade.exchange import (
|
||||
Bybit,
|
||||
Exchange,
|
||||
Kraken,
|
||||
date_minus_candles,
|
||||
market_is_active,
|
||||
timeframe_to_prev_date,
|
||||
)
|
||||
@@ -858,7 +859,7 @@ def test_validate_pricing(default_conf, mocker):
|
||||
default_conf["exchange"]["name"] = "binance"
|
||||
ExchangeResolver.load_exchange(default_conf)
|
||||
has.update({"fetchTicker": False})
|
||||
with pytest.raises(OperationalException, match="Ticker pricing not available for .*"):
|
||||
with pytest.raises(OperationalException, match=r"Ticker pricing not available for .*"):
|
||||
ExchangeResolver.load_exchange(default_conf)
|
||||
|
||||
has.update({"fetchTicker": True})
|
||||
@@ -867,7 +868,7 @@ def test_validate_pricing(default_conf, mocker):
|
||||
ExchangeResolver.load_exchange(default_conf)
|
||||
has.update({"fetchL2OrderBook": False})
|
||||
|
||||
with pytest.raises(OperationalException, match="Orderbook not available for .*"):
|
||||
with pytest.raises(OperationalException, match=r"Orderbook not available for .*"):
|
||||
ExchangeResolver.load_exchange(default_conf)
|
||||
|
||||
has.update({"fetchL2OrderBook": True})
|
||||
@@ -876,7 +877,7 @@ def test_validate_pricing(default_conf, mocker):
|
||||
default_conf["trading_mode"] = TradingMode.FUTURES
|
||||
default_conf["margin_mode"] = MarginMode.ISOLATED
|
||||
|
||||
with pytest.raises(OperationalException, match="Ticker pricing not available for .*"):
|
||||
with pytest.raises(OperationalException, match=r"Ticker pricing not available for .*"):
|
||||
ExchangeResolver.load_exchange(default_conf)
|
||||
|
||||
|
||||
@@ -2144,7 +2145,7 @@ def test___now_is_time_to_refresh(default_conf, mocker, exchange_name, time_mach
|
||||
assert exchange._now_is_time_to_refresh(pair, "1d", candle_type) is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize("candle_type", ["mark", ""])
|
||||
@pytest.mark.parametrize("candle_type", ["mark", "spot", "futures"])
|
||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||
def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name, candle_type):
|
||||
caplog.set_level(logging.DEBUG)
|
||||
@@ -2171,24 +2172,24 @@ def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name, candle_
|
||||
|
||||
exchange._async_get_candle_history = Mock(wraps=mock_candle_hist)
|
||||
# one_call calculation * 1.8 should do 2 calls
|
||||
candle_limit = exchange.ohlcv_candle_limit("5m", candle_type)
|
||||
since = date_minus_candles("5m", candle_limit)
|
||||
ret = exchange.get_historic_ohlcv(pair, "5m", dt_ts(since), candle_type=candle_type)
|
||||
|
||||
since = 5 * 60 * exchange.ohlcv_candle_limit("5m", candle_type) * 1.8
|
||||
ret = exchange.get_historic_ohlcv(
|
||||
pair, "5m", dt_ts(dt_now() - timedelta(seconds=since)), candle_type=candle_type
|
||||
)
|
||||
|
||||
assert exchange._async_get_candle_history.call_count == 2
|
||||
if exchange_name == "okx" and candle_type == "mark":
|
||||
expected = 4
|
||||
else:
|
||||
expected = 2
|
||||
assert exchange._async_get_candle_history.call_count == expected
|
||||
# Returns twice the above OHLCV data after truncating the open candle.
|
||||
assert len(ret) == 2
|
||||
assert len(ret) == expected
|
||||
assert log_has_re(r"Downloaded data for .* from ccxt with length .*\.", caplog)
|
||||
|
||||
caplog.clear()
|
||||
|
||||
exchange._async_get_candle_history = get_mock_coro(side_effect=TimeoutError())
|
||||
with pytest.raises(TimeoutError):
|
||||
exchange.get_historic_ohlcv(
|
||||
pair, "5m", dt_ts(dt_now() - timedelta(seconds=since)), candle_type=candle_type
|
||||
)
|
||||
exchange.get_historic_ohlcv(pair, "5m", dt_ts(since), candle_type=candle_type)
|
||||
assert log_has_re(r"Async code raised an exception: .*", caplog)
|
||||
|
||||
|
||||
@@ -2335,7 +2336,7 @@ def test_refresh_latest_ohlcv(mocker, default_conf_usdt, caplog, candle_type) ->
|
||||
if candle_type != CandleType.MARK:
|
||||
assert not res
|
||||
assert len(res) == 0
|
||||
assert log_has_re(r"Cannot download \(IOTA\/USDT, 3m\).*", caplog)
|
||||
assert log_has_re(r"Cannot download \(IOTA\/USDT, 3m, \S+\).*", caplog)
|
||||
else:
|
||||
assert len(res) == 1
|
||||
|
||||
@@ -3555,7 +3556,7 @@ def test_get_historic_trades_notsupported(
|
||||
pair = "ETH/BTC"
|
||||
|
||||
with pytest.raises(
|
||||
OperationalException, match="This exchange does not support downloading Trades."
|
||||
OperationalException, match=r"This exchange does not support downloading Trades\."
|
||||
):
|
||||
exchange.get_historic_trades(pair, since=trades_history[0][0], until=trades_history[-1][0])
|
||||
|
||||
@@ -4441,7 +4442,7 @@ def test_get_markets(
|
||||
def test_get_markets_error(default_conf, mocker):
|
||||
ex = get_patched_exchange(mocker, default_conf)
|
||||
mocker.patch(f"{EXMS}.markets", PropertyMock(return_value=None))
|
||||
with pytest.raises(OperationalException, match="Markets were not loaded."):
|
||||
with pytest.raises(OperationalException, match=r"Markets were not loaded\."):
|
||||
ex.get_markets("LTC", "USDT", True, False)
|
||||
|
||||
|
||||
@@ -4455,8 +4456,7 @@ def test_ohlcv_candle_limit(default_conf, mocker, exchange_name):
|
||||
for timeframe in timeframes:
|
||||
# if 'ohlcv_candle_limit_per_timeframe' in exchange._ft_has:
|
||||
# expected = exchange._ft_has['ohlcv_candle_limit_per_timeframe'][timeframe]
|
||||
# This should only run for bittrex
|
||||
# assert exchange_name == 'bittrex'
|
||||
# This should only run for htx
|
||||
assert exchange.ohlcv_candle_limit(timeframe, CandleType.SPOT) == expected
|
||||
|
||||
|
||||
@@ -5243,7 +5243,7 @@ def test__fetch_and_calculate_funding_fees(
|
||||
# Return empty "refresh_latest"
|
||||
mocker.patch(f"{EXMS}.refresh_latest_ohlcv", return_value={})
|
||||
ex = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange)
|
||||
with pytest.raises(ExchangeError, match="Could not find funding rates."):
|
||||
with pytest.raises(ExchangeError, match=r"Could not find funding rates\."):
|
||||
ex._fetch_and_calculate_funding_fees(
|
||||
pair="ADA/USDT:USDT", amount=amount, is_short=False, open_date=d1, close_date=d2
|
||||
)
|
||||
@@ -6319,3 +6319,39 @@ def test_exchange_features(default_conf, mocker):
|
||||
assert exchange.features("futures", "fetchOHLCV", "limit", 500) == 997
|
||||
# Fall back to default
|
||||
assert exchange.features("futures", "fetchOHLCV_else", "limit", 601) == 601
|
||||
|
||||
|
||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||
def test_fetch_funding_rate(default_conf, mocker, exchange_name):
|
||||
api_mock = MagicMock()
|
||||
funding_rate = {
|
||||
"symbol": "ETH/BTC",
|
||||
"fundingRate": 5.652e-05,
|
||||
"fundingTimestamp": 1757174400000,
|
||||
"fundingDatetime": "2025-09-06T16:00:00.000Z",
|
||||
}
|
||||
api_mock.fetch_funding_rate = MagicMock(return_value=funding_rate)
|
||||
api_mock.markets = {"ETH/BTC": {"active": True}}
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name)
|
||||
# retrieve original funding rate
|
||||
funding_rate = exchange.fetch_funding_rate(pair="ETH/BTC")
|
||||
assert funding_rate["fundingRate"] == funding_rate["fundingRate"]
|
||||
assert funding_rate["fundingTimestamp"] == funding_rate["fundingTimestamp"]
|
||||
assert funding_rate["fundingDatetime"] == funding_rate["fundingDatetime"]
|
||||
|
||||
ccxt_exceptionhandlers(
|
||||
mocker,
|
||||
default_conf,
|
||||
api_mock,
|
||||
exchange_name,
|
||||
"fetch_funding_rate",
|
||||
"fetch_funding_rate",
|
||||
pair="ETH/BTC",
|
||||
)
|
||||
|
||||
api_mock.fetch_funding_rate = MagicMock(return_value={})
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name)
|
||||
exchange.fetch_funding_rate(pair="ETH/BTC")
|
||||
|
||||
with pytest.raises(DependencyException, match=r"Pair XRP/ETH not available"):
|
||||
exchange.fetch_funding_rate(pair="XRP/ETH")
|
||||
|
||||
@@ -6,7 +6,8 @@ import pytest
|
||||
from tests.conftest import EXMS, get_mock_coro, get_patched_exchange
|
||||
|
||||
|
||||
def test_hyperliquid_dry_run_liquidation_price(default_conf, mocker):
|
||||
@pytest.mark.parametrize("margin_mode", ["isolated", "cross"])
|
||||
def test_hyperliquid_dry_run_liquidation_price(default_conf, mocker, margin_mode):
|
||||
# test if liq price calculated by dry_run_liquidation_price() is close to ccxt liq price
|
||||
# testing different pairs with large/small prices, different leverages, long, short
|
||||
markets = {
|
||||
@@ -281,7 +282,7 @@ def test_hyperliquid_dry_run_liquidation_price(default_conf, mocker):
|
||||
|
||||
api_mock = MagicMock()
|
||||
default_conf["trading_mode"] = "futures"
|
||||
default_conf["margin_mode"] = "isolated"
|
||||
default_conf["margin_mode"] = margin_mode
|
||||
default_conf["stake_currency"] = "USDC"
|
||||
api_mock.load_markets = get_mock_coro()
|
||||
api_mock.markets = markets
|
||||
@@ -299,11 +300,32 @@ def test_hyperliquid_dry_run_liquidation_price(default_conf, mocker):
|
||||
position["contracts"],
|
||||
position["collateral"],
|
||||
position["leverage"],
|
||||
position["collateral"],
|
||||
[],
|
||||
# isolated doesn't use wallet-balance
|
||||
wallet_balance=0.0 if margin_mode == "isolated" else position["collateral"],
|
||||
open_trades=[],
|
||||
)
|
||||
# Assume full position size is the wallet balance
|
||||
assert pytest.approx(liq_price_returned, rel=0.0001) == liq_price_calculated
|
||||
|
||||
if margin_mode == "cross":
|
||||
# test with larger wallet balance
|
||||
liq_price_calculated_cross = exchange.dry_run_liquidation_price(
|
||||
position["symbol"],
|
||||
position["entryPrice"],
|
||||
is_short,
|
||||
position["contracts"],
|
||||
position["collateral"],
|
||||
position["leverage"],
|
||||
wallet_balance=position["collateral"] * 2,
|
||||
open_trades=[],
|
||||
)
|
||||
# Assume full position size is the wallet balance
|
||||
# This
|
||||
if position["side"] == "long":
|
||||
assert liq_price_returned > liq_price_calculated_cross < position["entryPrice"]
|
||||
else:
|
||||
assert liq_price_returned < liq_price_calculated_cross > position["entryPrice"]
|
||||
|
||||
|
||||
def test_hyperliquid_get_funding_fees(default_conf, mocker):
|
||||
now = datetime.now(UTC)
|
||||
|
||||
@@ -20,11 +20,11 @@ def test_okx_ohlcv_candle_limit(default_conf, mocker):
|
||||
for timeframe in timeframes:
|
||||
assert exchange.ohlcv_candle_limit(timeframe, CandleType.SPOT) == 300
|
||||
assert exchange.ohlcv_candle_limit(timeframe, CandleType.FUTURES) == 300
|
||||
assert exchange.ohlcv_candle_limit(timeframe, CandleType.MARK) == 100
|
||||
assert exchange.ohlcv_candle_limit(timeframe, CandleType.MARK) == 300
|
||||
assert exchange.ohlcv_candle_limit(timeframe, CandleType.FUNDING_RATE) == 100
|
||||
|
||||
assert exchange.ohlcv_candle_limit(timeframe, CandleType.SPOT, start_time) == 100
|
||||
assert exchange.ohlcv_candle_limit(timeframe, CandleType.FUTURES, start_time) == 100
|
||||
assert exchange.ohlcv_candle_limit(timeframe, CandleType.SPOT, start_time) == 300
|
||||
assert exchange.ohlcv_candle_limit(timeframe, CandleType.FUTURES, start_time) == 300
|
||||
assert exchange.ohlcv_candle_limit(timeframe, CandleType.MARK, start_time) == 100
|
||||
assert exchange.ohlcv_candle_limit(timeframe, CandleType.FUNDING_RATE, start_time) == 100
|
||||
one_call = int(
|
||||
@@ -36,6 +36,7 @@ def test_okx_ohlcv_candle_limit(default_conf, mocker):
|
||||
|
||||
assert exchange.ohlcv_candle_limit(timeframe, CandleType.SPOT, one_call) == 300
|
||||
assert exchange.ohlcv_candle_limit(timeframe, CandleType.FUTURES, one_call) == 300
|
||||
assert exchange.ohlcv_candle_limit(timeframe, CandleType.MARK, one_call) == 300
|
||||
|
||||
one_call = int(
|
||||
(
|
||||
@@ -43,8 +44,9 @@ def test_okx_ohlcv_candle_limit(default_conf, mocker):
|
||||
).timestamp()
|
||||
* 1000
|
||||
)
|
||||
assert exchange.ohlcv_candle_limit(timeframe, CandleType.SPOT, one_call) == 100
|
||||
assert exchange.ohlcv_candle_limit(timeframe, CandleType.FUTURES, one_call) == 100
|
||||
assert exchange.ohlcv_candle_limit(timeframe, CandleType.SPOT, one_call) == 300
|
||||
assert exchange.ohlcv_candle_limit(timeframe, CandleType.FUTURES, one_call) == 300
|
||||
assert exchange.ohlcv_candle_limit(timeframe, CandleType.MARK, one_call) == 100
|
||||
|
||||
|
||||
def test_get_maintenance_ratio_and_amt_okx(
|
||||
|
||||
@@ -28,7 +28,11 @@ EXCHANGES = {
|
||||
"leverage_tiers_public": False,
|
||||
"leverage_in_spot_market": False,
|
||||
"trades_lookback_hours": 4,
|
||||
"private_methods": ["fapiPrivateGetPositionSideDual", "fapiPrivateGetMultiAssetsMargin"],
|
||||
"private_methods": [
|
||||
"fapiPrivateGetPositionSideDual",
|
||||
"fapiPrivateGetMultiAssetsMargin",
|
||||
"sapi_get_spot_delist_schedule",
|
||||
],
|
||||
"sample_order": [
|
||||
{
|
||||
"exchange_response": {
|
||||
@@ -149,6 +153,8 @@ EXCHANGES = {
|
||||
"ADA.F": {"balance": "2.00000000", "hold_trade": "0.00000000"},
|
||||
"XBT": {"balance": "0.00060000", "hold_trade": "0.00000000"},
|
||||
"XBT.F": {"balance": "0.00100000", "hold_trade": "0.00000000"},
|
||||
"ZEUR": {"balance": "1000.00000000", "hold_trade": "0.00000000"},
|
||||
"ZUSD": {"balance": "1000.00000000", "hold_trade": "0.00000000"},
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
@@ -157,6 +163,8 @@ EXCHANGES = {
|
||||
"BTC": {"free": 0.0006, "total": 0.0006, "used": 0.0},
|
||||
# XBT.F should be mapped to BTC.F
|
||||
"BTC.F": {"free": 0.001, "total": 0.001, "used": 0.0},
|
||||
"EUR": {"free": 1000.0, "total": 1000.0, "used": 0.0},
|
||||
"USD": {"free": 1000.0, "total": 1000.0, "used": 0.0},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -415,6 +423,14 @@ EXCHANGES = {
|
||||
"timeframe": "1h",
|
||||
"candle_count": 1000,
|
||||
},
|
||||
"coinex": {
|
||||
"pair": "BTC/USDT",
|
||||
"stake_currency": "USDT",
|
||||
"hasQuoteVolume": False,
|
||||
"timeframe": "1h",
|
||||
"candle_count": 1000,
|
||||
"orderbook_max_entries": 50,
|
||||
},
|
||||
# TODO: re-enable htx once certificates work again
|
||||
# "htx": {
|
||||
# "pair": "ETH/BTC",
|
||||
|
||||
@@ -986,7 +986,7 @@ def test_execute_entry(
|
||||
# Fail to get price...
|
||||
mocker.patch(f"{EXMS}.get_rate", MagicMock(return_value=0.0))
|
||||
|
||||
with pytest.raises(PricingError, match="Could not determine entry price."):
|
||||
with pytest.raises(PricingError, match=r"Could not determine entry price\."):
|
||||
freqtrade.execute_entry(pair, stake_amount, is_short=is_short)
|
||||
|
||||
# In case of custom entry price
|
||||
@@ -2267,6 +2267,18 @@ def test_manage_open_orders_exit_usercustom(
|
||||
freqtrade.manage_open_orders()
|
||||
assert log_has_re("Emergency exiting trade.*", caplog)
|
||||
assert et_mock.call_count == 1
|
||||
# Full exit
|
||||
assert et_mock.call_args_list[0][1]["sub_trade_amt"] == 30
|
||||
|
||||
et_mock.reset_mock()
|
||||
|
||||
# Full partially filled order
|
||||
# Only places the order for the remaining amount
|
||||
limit_sell_order_old["remaining"] = open_trade_usdt.amount - 10
|
||||
freqtrade.manage_open_orders()
|
||||
assert log_has_re("Emergency exiting trade.*", caplog)
|
||||
assert et_mock.call_count == 1
|
||||
assert et_mock.call_args_list[0][1]["sub_trade_amt"] == 20.0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("is_short", [False, True])
|
||||
|
||||
@@ -18,7 +18,7 @@ from tests.optimize import (
|
||||
)
|
||||
|
||||
|
||||
# Test 0: Sell with signal sell in candle 3
|
||||
# Test 0: exit with exit signal in candle 3
|
||||
# Test with Stop-loss at 1%
|
||||
tc0 = BTContainer(
|
||||
data=[
|
||||
@@ -279,7 +279,7 @@ tc12 = BTContainer(
|
||||
trades=[BTrade(exit_reason=ExitType.TRAILING_STOP_LOSS, open_tick=1, close_tick=2)],
|
||||
)
|
||||
|
||||
# Test 13: Buy and sell ROI on same candle
|
||||
# Test 13: Enter and exit ROI on same candle
|
||||
# stop-loss: 10% (should not apply), ROI: 1%
|
||||
tc13 = BTContainer(
|
||||
data=[
|
||||
@@ -296,7 +296,7 @@ tc13 = BTContainer(
|
||||
trades=[BTrade(exit_reason=ExitType.ROI, open_tick=1, close_tick=1)],
|
||||
)
|
||||
|
||||
# Test 14 - Buy and Stoploss on same candle
|
||||
# Test 14 - Enter and Stoploss on same candle
|
||||
# stop-loss: 5%, ROI: 10% (should not apply)
|
||||
tc14 = BTContainer(
|
||||
data=[
|
||||
@@ -314,7 +314,7 @@ tc14 = BTContainer(
|
||||
)
|
||||
|
||||
|
||||
# Test 15 - Buy and ROI on same candle, followed by buy and Stoploss on next candle
|
||||
# Test 15 - Enter and ROI on same candle, followed by entry and Stoploss on next candle
|
||||
# stop-loss: 5%, ROI: 10% (should not apply)
|
||||
tc15 = BTContainer(
|
||||
data=[
|
||||
@@ -334,8 +334,8 @@ tc15 = BTContainer(
|
||||
],
|
||||
)
|
||||
|
||||
# Test 16: Buy, hold for 65 min, then forceexit using roi=-1
|
||||
# Causes negative profit even though sell-reason is ROI.
|
||||
# Test 16: Enter, hold for 65 min, then forceexit using roi=-1
|
||||
# Causes negative profit even though exit-reason is ROI.
|
||||
# stop-loss: 10%, ROI: 10% (should not apply), -100% after 65 minutes (limits trade duration)
|
||||
tc16 = BTContainer(
|
||||
data=[
|
||||
@@ -353,10 +353,10 @@ tc16 = BTContainer(
|
||||
trades=[BTrade(exit_reason=ExitType.ROI, open_tick=1, close_tick=3)],
|
||||
)
|
||||
|
||||
# Test 17: Buy, hold for 120 mins, then forceexit using roi=-1
|
||||
# Causes negative profit even though sell-reason is ROI.
|
||||
# Test 17: Enter, hold for 120 mins, then forceexit using roi=-1
|
||||
# Causes negative profit even though exit-reason is ROI.
|
||||
# stop-loss: 10%, ROI: 10% (should not apply), -100% after 100 minutes (limits trade duration)
|
||||
# Uses open as sell-rate (special case) - since the roi-time is a multiple of the timeframe.
|
||||
# Uses open as exit-rate (special case) - since the roi-time is a multiple of the timeframe.
|
||||
tc17 = BTContainer(
|
||||
data=[
|
||||
# D O H L C V EL XL ES Xs BT
|
||||
@@ -374,16 +374,16 @@ tc17 = BTContainer(
|
||||
)
|
||||
|
||||
|
||||
# Test 18: Buy, hold for 120 mins, then drop ROI to 1%, causing a sell in candle 3.
|
||||
# Test 18: Enter, hold for 120 mins, then drop ROI to 1%, causing an exit in candle 3.
|
||||
# stop-loss: 10%, ROI: 10% (should not apply), -100% after 100 minutes (limits trade duration)
|
||||
# uses open_rate as sell-price
|
||||
# uses open_rate as exit price
|
||||
tc18 = BTContainer(
|
||||
data=[
|
||||
# D O H L C V EL XL ES Xs BT
|
||||
[0, 5000, 5025, 4975, 4987, 6172, 1, 0],
|
||||
[1, 5000, 5025, 4975, 4987, 6172, 0, 0],
|
||||
[2, 4987, 5300, 4950, 5200, 6172, 0, 0],
|
||||
[3, 5200, 5220, 4940, 4962, 6172, 0, 0], # Sell on ROI (sells on open)
|
||||
[3, 5200, 5220, 4940, 4962, 6172, 0, 0], # Exit on ROI (exits on open)
|
||||
[4, 4962, 4987, 4950, 4950, 6172, 0, 0],
|
||||
[5, 4950, 4975, 4925, 4950, 6172, 0, 0],
|
||||
],
|
||||
@@ -393,16 +393,16 @@ tc18 = BTContainer(
|
||||
trades=[BTrade(exit_reason=ExitType.ROI, open_tick=1, close_tick=3)],
|
||||
)
|
||||
|
||||
# Test 19: Buy, hold for 119 mins, then drop ROI to 1%, causing a sell in candle 3.
|
||||
# Test 19: Enter, hold for 119 mins, then drop ROI to 1%, causing an exit in candle 3.
|
||||
# stop-loss: 10%, ROI: 10% (should not apply), -100% after 100 minutes (limits trade duration)
|
||||
# uses calculated ROI (1%) as sell rate, otherwise identical to tc18
|
||||
# uses calculated ROI (1%) as exit rate, otherwise identical to tc18
|
||||
tc19 = BTContainer(
|
||||
data=[
|
||||
# D O H L C V EL XL ES Xs BT
|
||||
[0, 5000, 5025, 4975, 4987, 6172, 1, 0],
|
||||
[1, 5000, 5025, 4975, 4987, 6172, 0, 0],
|
||||
[2, 4987, 5300, 4950, 5200, 6172, 0, 0],
|
||||
[3, 5000, 5300, 4940, 4962, 6172, 0, 0], # Sell on ROI
|
||||
[3, 5000, 5300, 4940, 4962, 6172, 0, 0], # Exit on ROI
|
||||
[4, 4962, 4987, 4950, 4950, 6172, 0, 0],
|
||||
[5, 4550, 4975, 4550, 4950, 6172, 0, 0],
|
||||
],
|
||||
@@ -412,16 +412,16 @@ tc19 = BTContainer(
|
||||
trades=[BTrade(exit_reason=ExitType.ROI, open_tick=1, close_tick=3)],
|
||||
)
|
||||
|
||||
# Test 20: Buy, hold for 119 mins, then drop ROI to 1%, causing a sell in candle 3.
|
||||
# Test 20: Enter, hold for 119 mins, then drop ROI to 1%, causing an exit in candle 3.
|
||||
# stop-loss: 10%, ROI: 10% (should not apply), -100% after 100 minutes (limits trade duration)
|
||||
# uses calculated ROI (1%) as sell rate, otherwise identical to tc18
|
||||
# uses calculated ROI (1%) as exit rate, otherwise identical to tc18
|
||||
tc20 = BTContainer(
|
||||
data=[
|
||||
# D O H L C V EL XL ES Xs BT
|
||||
[0, 5000, 5025, 4975, 4987, 6172, 1, 0],
|
||||
[1, 5000, 5025, 4975, 4987, 6172, 0, 0],
|
||||
[2, 4987, 5300, 4950, 5200, 6172, 0, 0],
|
||||
[3, 5200, 5300, 4940, 4962, 6172, 0, 0], # Sell on ROI
|
||||
[3, 5200, 5300, 4940, 4962, 6172, 0, 0], # Exit on ROI
|
||||
[4, 4962, 4987, 4950, 4950, 6172, 0, 0],
|
||||
[5, 4925, 4975, 4925, 4950, 6172, 0, 0],
|
||||
],
|
||||
@@ -434,7 +434,7 @@ tc20 = BTContainer(
|
||||
# Test 21: trailing_stop ROI collision.
|
||||
# Roi should trigger before Trailing stop - otherwise Trailing stop profits can be > ROI
|
||||
# which cannot happen in reality
|
||||
# stop-loss: 10%, ROI: 4%, Trailing stop adjusted at the sell candle
|
||||
# stop-loss: 10%, ROI: 4%, Trailing stop adjusted at the exit candle
|
||||
tc21 = BTContainer(
|
||||
data=[
|
||||
# D O H L C V EL XL ES Xs BT
|
||||
@@ -501,10 +501,10 @@ tc23 = BTContainer(
|
||||
|
||||
# Test 24: trailing_stop Raises in candle 2 (does not trigger)
|
||||
# applying a positive trailing stop of 3% since stop_positive_offset is reached.
|
||||
# ROI is changed after this to 4%, dropping ROI below trailing_stop_positive, causing a sell
|
||||
# ROI is changed after this to 4%, dropping ROI below trailing_stop_positive, causing an exit
|
||||
# in the candle after the raised stoploss candle with ROI reason.
|
||||
# Stoploss would trigger in this candle too, but it's no longer relevant.
|
||||
# stop-loss: 10%, ROI: 4%, stoploss adjusted candle 2, ROI adjusted in candle 3 (causing the sell)
|
||||
# stop-loss: 10%, ROI: 4%, stoploss adjusted candle 2, ROI adjusted in candle 3 (causing the exit)
|
||||
tc24 = BTContainer(
|
||||
data=[
|
||||
# D O H L C V EL XL ES Xs BT
|
||||
@@ -524,16 +524,16 @@ tc24 = BTContainer(
|
||||
trades=[BTrade(exit_reason=ExitType.ROI, open_tick=1, close_tick=3)],
|
||||
)
|
||||
|
||||
# Test 25: Sell with signal sell in candle 3 (stoploss also triggers on this candle)
|
||||
# Test 25: Exit with exit signal in candle 3 (stoploss also triggers on this candle)
|
||||
# Stoploss at 1%.
|
||||
# Stoploss wins over Sell-signal (because sell-signal is acted on in the next candle)
|
||||
# Stoploss wins over exit-signal (because exit-signal is acted on in the next candle)
|
||||
tc25 = BTContainer(
|
||||
data=[
|
||||
# D O H L C V EL XL ES Xs BT
|
||||
[0, 5000, 5025, 4975, 4987, 6172, 1, 0],
|
||||
[1, 5000, 5025, 4975, 4987, 6172, 0, 0], # enter trade (signal on last candle)
|
||||
[2, 4987, 5012, 4986, 4986, 6172, 0, 0],
|
||||
[3, 5010, 5010, 4855, 5010, 6172, 0, 1], # Triggers stoploss + sellsignal
|
||||
[3, 5010, 5010, 4855, 5010, 6172, 0, 1], # Triggers stoploss + exit-signal
|
||||
[4, 5010, 5010, 4977, 4995, 6172, 0, 0],
|
||||
[5, 4995, 4995, 4950, 4950, 6172, 0, 0],
|
||||
],
|
||||
@@ -544,9 +544,9 @@ tc25 = BTContainer(
|
||||
trades=[BTrade(exit_reason=ExitType.STOP_LOSS, open_tick=1, close_tick=3)],
|
||||
)
|
||||
|
||||
# Test 26: Sell with signal sell in candle 3 (stoploss also triggers on this candle)
|
||||
# Test 26: Exit with exit signal in candle 3 (stoploss also triggers on this candle)
|
||||
# Stoploss at 1%.
|
||||
# Sell-signal wins over stoploss
|
||||
# Exit-signal wins over stoploss
|
||||
tc26 = BTContainer(
|
||||
data=[
|
||||
# D O H L C V EL XL ES Xs BT
|
||||
@@ -554,7 +554,7 @@ tc26 = BTContainer(
|
||||
[1, 5000, 5025, 4975, 4987, 6172, 0, 0], # enter trade (signal on last candle)
|
||||
[2, 4987, 5012, 4986, 4986, 6172, 0, 0],
|
||||
[3, 5010, 5010, 4986, 5010, 6172, 0, 1],
|
||||
[4, 5010, 5010, 4855, 4995, 6172, 0, 0], # Triggers stoploss + sellsignal acted on
|
||||
[4, 5010, 5010, 4855, 4995, 6172, 0, 0], # Triggers stoploss + exit-signal acted on
|
||||
[5, 4995, 4995, 4950, 4950, 6172, 0, 0],
|
||||
],
|
||||
stop_loss=-0.01,
|
||||
@@ -565,9 +565,9 @@ tc26 = BTContainer(
|
||||
)
|
||||
|
||||
# Test 27: (copy of test26 with leverage)
|
||||
# Sell with signal sell in candle 3 (stoploss also triggers on this candle)
|
||||
# Exit with exit signal in candle 3 (stoploss also triggers on this candle)
|
||||
# Stoploss at 1%.
|
||||
# Sell-signal wins over stoploss
|
||||
# exit-signal wins over stoploss
|
||||
tc27 = BTContainer(
|
||||
data=[
|
||||
# D O H L C V EL XL ES Xs BT
|
||||
@@ -575,7 +575,7 @@ tc27 = BTContainer(
|
||||
[1, 5000, 5025, 4975, 4987, 6172, 0, 0], # enter trade (signal on last candle)
|
||||
[2, 4987, 5012, 4986, 4986, 6172, 0, 0],
|
||||
[3, 5010, 5010, 4986, 5010, 6172, 0, 1],
|
||||
[4, 5010, 5010, 4855, 4995, 6172, 0, 0], # Triggers stoploss + sellsignal acted on
|
||||
[4, 5010, 5010, 4855, 4995, 6172, 0, 0], # Triggers stoploss + exit-signal acted on
|
||||
[5, 4995, 4995, 4950, 4950, 6172, 0, 0],
|
||||
],
|
||||
stop_loss=-0.05,
|
||||
@@ -587,9 +587,9 @@ tc27 = BTContainer(
|
||||
)
|
||||
|
||||
# Test 28: (copy of test26 with leverage and as short)
|
||||
# Sell with signal sell in candle 3 (stoploss also triggers on this candle)
|
||||
# Exit with exit signal in candle 3 (stoploss also triggers on this candle)
|
||||
# Stoploss at 1%.
|
||||
# Sell-signal wins over stoploss
|
||||
# Exit-signal wins over stoploss
|
||||
tc28 = BTContainer(
|
||||
data=[
|
||||
# D O H L C V EL XL ES Xs BT
|
||||
@@ -597,7 +597,7 @@ tc28 = BTContainer(
|
||||
[1, 5000, 5025, 4975, 4987, 6172, 0, 0, 0, 0], # enter trade (signal on last candle)
|
||||
[2, 4987, 5012, 4986, 4986, 6172, 0, 0, 0, 0],
|
||||
[3, 5010, 5010, 4986, 5010, 6172, 0, 0, 0, 1],
|
||||
[4, 4990, 5010, 4855, 4995, 6172, 0, 0, 0, 0], # Triggers stoploss + sellsignal acted on
|
||||
[4, 4990, 5010, 4855, 4995, 6172, 0, 0, 0, 0], # Triggers stoploss + exit-signal acted on
|
||||
[5, 4995, 4995, 4950, 4950, 6172, 0, 0, 0, 0],
|
||||
],
|
||||
stop_loss=-0.05,
|
||||
@@ -607,16 +607,16 @@ tc28 = BTContainer(
|
||||
leverage=5.0,
|
||||
trades=[BTrade(exit_reason=ExitType.EXIT_SIGNAL, open_tick=1, close_tick=4, is_short=True)],
|
||||
)
|
||||
# Test 29: Sell with signal sell in candle 3 (ROI at signal candle)
|
||||
# Test 29: Exit with exit signal in candle 3 (ROI at signal candle)
|
||||
# Stoploss at 10% (irrelevant), ROI at 5% (will trigger)
|
||||
# Sell-signal wins over stoploss
|
||||
# Exit-signal wins over stoploss
|
||||
tc29 = BTContainer(
|
||||
data=[
|
||||
# D O H L C V EL XL ES Xs BT
|
||||
[0, 5000, 5025, 4975, 4987, 6172, 1, 0],
|
||||
[1, 5000, 5025, 4975, 4987, 6172, 0, 0], # enter trade (signal on last candle)
|
||||
[2, 4987, 5012, 4986, 4986, 6172, 0, 0],
|
||||
[3, 5010, 5251, 4986, 5010, 6172, 0, 1], # Triggers ROI, sell-signal
|
||||
[3, 5010, 5251, 4986, 5010, 6172, 0, 1], # Triggers ROI, exit-signal
|
||||
[4, 5010, 5010, 4855, 4995, 6172, 0, 0],
|
||||
[5, 4995, 4995, 4950, 4950, 6172, 0, 0],
|
||||
],
|
||||
@@ -627,16 +627,16 @@ tc29 = BTContainer(
|
||||
trades=[BTrade(exit_reason=ExitType.ROI, open_tick=1, close_tick=3)],
|
||||
)
|
||||
|
||||
# Test 30: Sell with signal sell in candle 3 (ROI at signal candle)
|
||||
# Stoploss at 10% (irrelevant), ROI at 5% (will trigger) - Wins over Sell-signal
|
||||
# Test 30: Exit with exit signal in candle 3 (ROI at signal candle)
|
||||
# Stoploss at 10% (irrelevant), ROI at 5% (will trigger) - Wins over exit-signal
|
||||
tc30 = BTContainer(
|
||||
data=[
|
||||
# D O H L C V EL XL ES Xs BT
|
||||
[0, 5000, 5025, 4975, 4987, 6172, 1, 0],
|
||||
[1, 5000, 5025, 4975, 4987, 6172, 0, 0], # enter trade (signal on last candle)
|
||||
[2, 4987, 5012, 4986, 4986, 6172, 0, 0],
|
||||
[3, 5010, 5012, 4986, 5010, 6172, 0, 1], # sell-signal
|
||||
[4, 5010, 5251, 4855, 4995, 6172, 0, 0], # Triggers ROI, sell-signal acted on
|
||||
[3, 5010, 5012, 4986, 5010, 6172, 0, 1], # exit-signal
|
||||
[4, 5010, 5251, 4855, 4995, 6172, 0, 0], # Triggers ROI, exit-signal acted on
|
||||
[5, 4995, 4995, 4950, 4950, 6172, 0, 0],
|
||||
],
|
||||
stop_loss=-0.10,
|
||||
@@ -888,7 +888,7 @@ tc41 = BTContainer(
|
||||
|
||||
# Test 42: Custom-entry-price around candle low
|
||||
# Would cause immediate ROI exit, but since the trade was entered
|
||||
# below open, we treat this as cheating, and delay the sell by 1 candle.
|
||||
# below open, we treat this as cheating, and delay the exit by 1 candle.
|
||||
# details: https://github.com/freqtrade/freqtrade/issues/6261
|
||||
tc42 = BTContainer(
|
||||
data=[
|
||||
@@ -945,7 +945,7 @@ tc44 = BTContainer(
|
||||
)
|
||||
|
||||
# Test 45: Custom exit price above all candles
|
||||
# causes sell signal timeout
|
||||
# causes exit signal timeout
|
||||
tc45 = BTContainer(
|
||||
data=[
|
||||
# D O H L C V EL XL ES Xs BT
|
||||
@@ -964,7 +964,7 @@ tc45 = BTContainer(
|
||||
)
|
||||
|
||||
# Test 46: (Short of tc45) Custom short exit price above below candles
|
||||
# causes sell signal timeout
|
||||
# causes exit signal timeout
|
||||
tc46 = BTContainer(
|
||||
data=[
|
||||
# D O H L C V EL XL ES Xs BT
|
||||
|
||||
@@ -27,7 +27,7 @@ from freqtrade.optimize.backtest_caching import get_backtest_metadata_filename,
|
||||
from freqtrade.optimize.backtesting import Backtesting
|
||||
from freqtrade.persistence import LocalTrade, Trade
|
||||
from freqtrade.resolvers import StrategyResolver
|
||||
from freqtrade.util.datetime_helpers import dt_utc
|
||||
from freqtrade.util import dt_now, dt_utc
|
||||
from tests.conftest import (
|
||||
CURRENT_TEST_STRATEGY,
|
||||
EXMS,
|
||||
@@ -357,7 +357,7 @@ def test_get_pair_precision_bt(default_conf, mocker) -> None:
|
||||
pair = "UNITTEST/BTC"
|
||||
backtesting.pairlists._whitelist = [pair]
|
||||
ex_mock = mocker.patch(f"{EXMS}.get_precision_price", return_value=1e-5)
|
||||
data, timerange = backtesting.load_bt_data()
|
||||
data, _timerange = backtesting.load_bt_data()
|
||||
assert data
|
||||
|
||||
assert backtesting.get_pair_precision(pair, dt_utc(2018, 1, 1)) == (1e-8, TICK_SIZE)
|
||||
@@ -444,7 +444,7 @@ def test_backtesting_start_no_data(default_conf, mocker, caplog, testdatadir) ->
|
||||
|
||||
backtesting = Backtesting(default_conf)
|
||||
backtesting._set_strategy(backtesting.strategylist[0])
|
||||
with pytest.raises(OperationalException, match="No data found. Terminating."):
|
||||
with pytest.raises(OperationalException, match=r"No data found. Terminating\."):
|
||||
backtesting.start()
|
||||
|
||||
|
||||
@@ -465,7 +465,7 @@ def test_backtesting_no_pair_left(default_conf, mocker) -> None:
|
||||
default_conf["export"] = "none"
|
||||
default_conf["timerange"] = "20180101-20180102"
|
||||
|
||||
with pytest.raises(OperationalException, match="No pair in whitelist."):
|
||||
with pytest.raises(OperationalException, match=r"No pair in whitelist\."):
|
||||
Backtesting(default_conf)
|
||||
|
||||
default_conf.update(
|
||||
@@ -476,7 +476,7 @@ def test_backtesting_no_pair_left(default_conf, mocker) -> None:
|
||||
)
|
||||
|
||||
with pytest.raises(
|
||||
OperationalException, match="Detail timeframe must be smaller than strategy timeframe."
|
||||
OperationalException, match=r"Detail timeframe must be smaller than strategy timeframe\."
|
||||
):
|
||||
Backtesting(default_conf)
|
||||
|
||||
@@ -517,7 +517,7 @@ def test_backtesting_pairlist_list(default_conf, mocker, tickers) -> None:
|
||||
default_conf["strategy_list"] = [CURRENT_TEST_STRATEGY, "StrategyTestV2"]
|
||||
with pytest.raises(
|
||||
OperationalException,
|
||||
match="PrecisionFilter not allowed for backtesting multiple strategies.",
|
||||
match=r"PrecisionFilter not allowed for backtesting multiple strategies\.",
|
||||
):
|
||||
Backtesting(default_conf)
|
||||
|
||||
@@ -2715,3 +2715,75 @@ def test_get_backtest_metadata_filename():
|
||||
filename = "backtest_results_zip.zip"
|
||||
expected = Path("backtest_results_zip.meta.json")
|
||||
assert get_backtest_metadata_filename(filename) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dynamic_pairlist", [True, False])
|
||||
def test_time_pair_generator_refresh_pairlist(mocker, default_conf, dynamic_pairlist):
|
||||
patch_exchange(mocker)
|
||||
default_conf["enable_dynamic_pairlist"] = dynamic_pairlist
|
||||
backtesting = Backtesting(default_conf)
|
||||
backtesting._set_strategy(backtesting.strategylist[0])
|
||||
assert backtesting.dynamic_pairlist == dynamic_pairlist
|
||||
|
||||
refresh_mock = mocker.patch(
|
||||
"freqtrade.plugins.pairlistmanager.PairListManager.refresh_pairlist"
|
||||
)
|
||||
|
||||
# Simulate 2 candles
|
||||
start_date = datetime(2025, 1, 1, 0, 0, tzinfo=UTC)
|
||||
end_date = start_date + timedelta(minutes=10)
|
||||
pairs = default_conf["exchange"]["pair_whitelist"]
|
||||
data = {pair: [] for pair in pairs}
|
||||
|
||||
# Simulate backtest loop
|
||||
list(backtesting.time_pair_generator(start_date, end_date, pairs, data))
|
||||
|
||||
if dynamic_pairlist:
|
||||
assert refresh_mock.call_count == 2
|
||||
else:
|
||||
assert refresh_mock.call_count == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dynamic_pairlist", [True, False])
|
||||
def test_time_pair_generator_open_trades_first(mocker, default_conf, dynamic_pairlist):
|
||||
patch_exchange(mocker)
|
||||
default_conf["enable_dynamic_pairlist"] = dynamic_pairlist
|
||||
backtesting = Backtesting(default_conf)
|
||||
backtesting._set_strategy(backtesting.strategylist[0])
|
||||
assert backtesting.dynamic_pairlist == dynamic_pairlist
|
||||
|
||||
pairs = ["XRP/BTC", "LTC/BTC", "NEO/BTC", "ETH/BTC"]
|
||||
|
||||
# Simulate open trades
|
||||
trades = [
|
||||
LocalTrade(pair="XRP/BTC", open_date=dt_now(), amount=1, open_rate=1),
|
||||
LocalTrade(pair="NEO/BTC", open_date=dt_now(), amount=1, open_rate=1),
|
||||
]
|
||||
LocalTrade.bt_trades_open = trades
|
||||
LocalTrade.bt_trades_open_pp = {
|
||||
"XRP/BTC": [trades[0]],
|
||||
"NEO/BTC": [trades[1]],
|
||||
"LTC/BTC": [],
|
||||
"ETH/BTC": [],
|
||||
}
|
||||
|
||||
start_date = datetime(2025, 1, 1, 0, 0, tzinfo=UTC)
|
||||
end_date = start_date + timedelta(minutes=5)
|
||||
dummy_row = (end_date, 1.0, 1.1, 0.9, 1.0, 0, 0, 0, 0, None, None)
|
||||
data = {pair: [dummy_row] for pair in pairs}
|
||||
|
||||
def mock_refresh(self):
|
||||
# Simulate shuffle
|
||||
self._whitelist = pairs[::-1] # ['ETH/BTC', 'NEO/BTC', 'LTC/BTC', 'XRP/BTC']
|
||||
|
||||
mocker.patch("freqtrade.plugins.pairlistmanager.PairListManager.refresh_pairlist", mock_refresh)
|
||||
|
||||
processed_pairs = []
|
||||
for _, pair, _, _, _ in backtesting.time_pair_generator(start_date, end_date, pairs, data):
|
||||
processed_pairs.append(pair)
|
||||
|
||||
# Open trades first in both cases
|
||||
if dynamic_pairlist:
|
||||
assert processed_pairs == ["XRP/BTC", "NEO/BTC", "ETH/BTC", "LTC/BTC"]
|
||||
else:
|
||||
assert processed_pairs == ["XRP/BTC", "NEO/BTC", "LTC/BTC", "ETH/BTC"]
|
||||
|
||||
@@ -280,7 +280,7 @@ def test_start_no_data(mocker, hyperopt_conf, tmp_path) -> None:
|
||||
"5",
|
||||
]
|
||||
pargs = get_args(args)
|
||||
with pytest.raises(OperationalException, match="No data found. Terminating."):
|
||||
with pytest.raises(OperationalException, match=r"No data found. Terminating\."):
|
||||
start_hyperopt(pargs)
|
||||
|
||||
# Cleanup since that failed hyperopt start leaves a lockfile.
|
||||
@@ -1127,7 +1127,7 @@ def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmp_path, fee) -> None
|
||||
assert opt.backtesting.strategy.max_open_trades != 1
|
||||
|
||||
opt.custom_hyperopt.generate_estimator = lambda *args, **kwargs: "ET1"
|
||||
with pytest.raises(OperationalException, match="Optuna Sampler ET1 not supported."):
|
||||
with pytest.raises(OperationalException, match=r"Optuna Sampler ET1 not supported\."):
|
||||
opt.get_optimizer(42)
|
||||
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@ from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver
|
||||
def test_hyperoptlossresolver_noname(default_conf):
|
||||
with pytest.raises(
|
||||
OperationalException,
|
||||
match="No Hyperopt loss set. Please use `--hyperopt-loss` to specify "
|
||||
"the Hyperopt-Loss class to use.",
|
||||
match=r"No Hyperopt loss set. Please use `--hyperopt-loss` to specify "
|
||||
r"the Hyperopt-Loss class to use\.",
|
||||
):
|
||||
HyperOptLossResolver.load_hyperoptloss(default_conf)
|
||||
|
||||
|
||||
@@ -123,7 +123,7 @@ def test_lookahead_helper_no_strategy_defined(lookahead_conf):
|
||||
LookaheadAnalysisSubFunctions.start(conf)
|
||||
|
||||
|
||||
def test_lookahead_helper_start(lookahead_conf, mocker) -> None:
|
||||
def test_lookahead_helper_start(lookahead_conf, mocker, caplog) -> None:
|
||||
single_mock = MagicMock()
|
||||
text_table_mock = MagicMock()
|
||||
mocker.patch.multiple(
|
||||
@@ -131,13 +131,22 @@ def test_lookahead_helper_start(lookahead_conf, mocker) -> None:
|
||||
initialize_single_lookahead_analysis=single_mock,
|
||||
text_table_lookahead_analysis_instances=text_table_mock,
|
||||
)
|
||||
LookaheadAnalysisSubFunctions.start(lookahead_conf)
|
||||
LookaheadAnalysisSubFunctions.start(deepcopy(lookahead_conf))
|
||||
assert single_mock.call_count == 1
|
||||
assert text_table_mock.call_count == 1
|
||||
assert log_has_re("Forced order_types to market orders.", caplog)
|
||||
assert single_mock.call_args_list[0][0][0]["order_types"]["entry"] == "market"
|
||||
|
||||
single_mock.reset_mock()
|
||||
text_table_mock.reset_mock()
|
||||
|
||||
lookahead_conf["lookahead_allow_limit_orders"] = True
|
||||
LookaheadAnalysisSubFunctions.start(lookahead_conf)
|
||||
assert single_mock.call_count == 1
|
||||
assert text_table_mock.call_count == 1
|
||||
assert log_has_re("Using configured order_types, skipping order_types override.", caplog)
|
||||
assert "order_types" not in single_mock.call_args_list[0][0][0]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"indicators, expected_caption_text",
|
||||
|
||||
@@ -18,7 +18,7 @@ from freqtrade.persistence import LocalTrade, 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 freqtrade.util.datetime_helpers import dt_now
|
||||
from freqtrade.util import dt_now, dt_utc
|
||||
from tests.conftest import (
|
||||
EXMS,
|
||||
create_mock_trades_usdt,
|
||||
@@ -1274,27 +1274,37 @@ def test_ShuffleFilter_init(mocker, whitelist_conf, caplog) -> None:
|
||||
{"method": "StaticPairList"},
|
||||
{"method": "ShuffleFilter", "seed": 43},
|
||||
]
|
||||
whitelist_conf["runmode"] = "backtest"
|
||||
whitelist_conf["runmode"] = RunMode.BACKTEST
|
||||
|
||||
exchange = get_patched_exchange(mocker, whitelist_conf)
|
||||
plm = PairListManager(exchange, whitelist_conf)
|
||||
assert log_has("Backtesting mode detected, applying seed value: 43", caplog)
|
||||
|
||||
plm.refresh_pairlist()
|
||||
pl1 = deepcopy(plm.whitelist)
|
||||
plm.refresh_pairlist()
|
||||
assert plm.whitelist != pl1
|
||||
assert set(plm.whitelist) == set(pl1)
|
||||
|
||||
caplog.clear()
|
||||
whitelist_conf["runmode"] = RunMode.DRY_RUN
|
||||
plm = PairListManager(exchange, whitelist_conf)
|
||||
assert not log_has("Backtesting mode detected, applying seed value: 42", caplog)
|
||||
assert log_has("Live mode detected, not applying seed.", caplog)
|
||||
|
||||
with time_machine.travel("2021-09-01 05:01:00 +00:00") as t:
|
||||
plm.refresh_pairlist()
|
||||
pl1 = deepcopy(plm.whitelist)
|
||||
plm.refresh_pairlist()
|
||||
assert plm.whitelist == pl1
|
||||
|
||||
target = plm._pairlist_handlers[1]._random
|
||||
shuffle_mock = mocker.patch.object(target, "shuffle", wraps=target.shuffle)
|
||||
|
||||
t.shift(timedelta(minutes=10))
|
||||
plm.refresh_pairlist()
|
||||
assert plm.whitelist != pl1
|
||||
|
||||
caplog.clear()
|
||||
whitelist_conf["runmode"] = RunMode.DRY_RUN
|
||||
plm = PairListManager(exchange, whitelist_conf)
|
||||
assert not log_has("Backtesting mode detected, applying seed value: 42", caplog)
|
||||
assert log_has("Live mode detected, not applying seed.", caplog)
|
||||
assert shuffle_mock.call_count == 1
|
||||
assert set(plm.whitelist) == set(pl1)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_persistence")
|
||||
@@ -1669,7 +1679,7 @@ def test_rangestabilityfilter_checks(mocker, default_conf, markets, tickers):
|
||||
|
||||
with pytest.raises(
|
||||
OperationalException,
|
||||
match="RangeStabilityFilter requires sort_direction to be either None.*",
|
||||
match=r"RangeStabilityFilter requires sort_direction to be either None\.*",
|
||||
):
|
||||
get_patched_freqtradebot(mocker, default_conf)
|
||||
|
||||
@@ -1823,6 +1833,17 @@ def test_spreadfilter_invalid_data(mocker, default_conf, markets, tickers, caplo
|
||||
None,
|
||||
"PriceFilter requires max_value to be >= 0",
|
||||
), # OperationalException expected
|
||||
(
|
||||
{"method": "DelistFilter", "max_days_from_now": -1},
|
||||
None,
|
||||
"DelistFilter requires max_days_from_now to be >= 0",
|
||||
), # ConfigurationError expected
|
||||
(
|
||||
{"method": "DelistFilter", "max_days_from_now": 1},
|
||||
"[{'DelistFilter': 'DelistFilter - Filtering pairs that will be delisted in the "
|
||||
"next 1 days.'}]",
|
||||
None,
|
||||
), # ConfigurationError expected
|
||||
(
|
||||
{"method": "RangeStabilityFilter", "lookback_days": 10, "min_rate_of_change": 0.01},
|
||||
"[{'RangeStabilityFilter': 'RangeStabilityFilter - Filtering pairs with rate "
|
||||
@@ -2526,7 +2547,7 @@ def test_MarketCapPairList_exceptions(mocker, default_conf_usdt, caplog):
|
||||
}
|
||||
]
|
||||
with pytest.raises(
|
||||
OperationalException, match="Category layer250 not in coingecko category list."
|
||||
OperationalException, match=r"Category layer250 not in coingecko category list\."
|
||||
):
|
||||
PairListManager(exchange, default_conf_usdt)
|
||||
|
||||
@@ -2591,3 +2612,63 @@ def test_backtesting_modes(
|
||||
|
||||
if expected_warning:
|
||||
assert log_has_re(f"Pairlist Handlers {expected_warning}", caplog)
|
||||
|
||||
|
||||
def test_DelistFilter_error(whitelist_conf) -> None:
|
||||
whitelist_conf["pairlists"] = [{"method": "StaticPairList"}, {"method": "DelistFilter"}]
|
||||
exchange_mock = MagicMock()
|
||||
exchange_mock._ft_has = {"has_delisting": False}
|
||||
with pytest.raises(
|
||||
OperationalException,
|
||||
match=r"DelistFilter doesn't support this exchange and trading mode combination\.",
|
||||
):
|
||||
PairListManager(exchange_mock, whitelist_conf, MagicMock())
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_persistence")
|
||||
def test_DelistFilter(mocker, default_conf_usdt, time_machine, caplog) -> None:
|
||||
default_conf_usdt["exchange"]["pair_whitelist"] = [
|
||||
"ETH/USDT",
|
||||
"XRP/USDT",
|
||||
"BTC/USDT",
|
||||
"NEO/USDT",
|
||||
]
|
||||
default_conf_usdt["pairlists"] = [
|
||||
{"method": "StaticPairList"},
|
||||
{"method": "DelistFilter", "max_days_from_now": 3},
|
||||
]
|
||||
default_conf_usdt["max_open_trades"] = -1
|
||||
exchange = get_patched_exchange(mocker, default_conf_usdt)
|
||||
|
||||
def delist_mock(pair: str):
|
||||
mock_delist = {
|
||||
"XRP/USDT": dt_utc(2025, 9, 1) + timedelta(days=1), # Delisting in 1 day
|
||||
"NEO/USDT": dt_utc(2025, 9, 1) + timedelta(days=5, hours=2), # Delisting in 5 days
|
||||
}
|
||||
return mock_delist.get(pair, None)
|
||||
|
||||
time_machine.move_to("2025-09-01 01:00:00 +00:00", tick=False)
|
||||
|
||||
mocker.patch.object(exchange, "check_delisting_time", delist_mock)
|
||||
pm = PairListManager(exchange, default_conf_usdt)
|
||||
pm.refresh_pairlist()
|
||||
assert pm.whitelist == ["ETH/USDT", "BTC/USDT", "NEO/USDT"]
|
||||
assert log_has(
|
||||
"Removed XRP/USDT from whitelist, because it will be delisted on 2025-09-02 00:00:00.",
|
||||
caplog,
|
||||
)
|
||||
# NEO is kept initially as delisting is in 5 days, but config is 3 days
|
||||
|
||||
time_machine.move_to("2025-09-03 01:00:00 +00:00", tick=False)
|
||||
pm.refresh_pairlist()
|
||||
assert pm.whitelist == ["ETH/USDT", "BTC/USDT", "NEO/USDT"]
|
||||
# NEO not removed yet, expiry falls into the window 1 hour later
|
||||
|
||||
time_machine.move_to("2025-09-03 02:00:00 +00:00", tick=False)
|
||||
pm.refresh_pairlist()
|
||||
assert pm.whitelist == ["ETH/USDT", "BTC/USDT"]
|
||||
|
||||
assert log_has(
|
||||
"Removed NEO/USDT from whitelist, because it will be delisted on 2025-09-06 02:00:00.",
|
||||
caplog,
|
||||
)
|
||||
|
||||
@@ -82,7 +82,7 @@ def test_fetch_pairlist_mock_response_html(mocker, rpl_config):
|
||||
exchange, pairlistmanager, rpl_config, rpl_config["pairlists"][0], 0
|
||||
)
|
||||
|
||||
with pytest.raises(OperationalException, match="RemotePairList is not of type JSON."):
|
||||
with pytest.raises(OperationalException, match=r"RemotePairList is not of type JSON\."):
|
||||
remote_pairlist.fetch_pairlist()
|
||||
|
||||
|
||||
|
||||
@@ -393,7 +393,7 @@ def test_rpc_delete_trade(mocker, default_conf, fee, markets, caplog, is_short):
|
||||
freqtradebot.strategy.order_types["stoploss_on_exchange"] = True
|
||||
create_mock_trades(fee, is_short)
|
||||
rpc = RPC(freqtradebot)
|
||||
with pytest.raises(RPCException, match="Trade with id '200' not found."):
|
||||
with pytest.raises(RPCException, match=r"Trade with id '200' not found\."):
|
||||
rpc._rpc_delete("200")
|
||||
|
||||
trades = Trade.session.scalars(select(Trade)).all()
|
||||
@@ -1204,7 +1204,7 @@ def test_rpc_force_entry(mocker, default_conf, ticker, fee, limit_buy_order_open
|
||||
patch_get_signal(freqtradebot)
|
||||
rpc = RPC(freqtradebot)
|
||||
pair = "ETH/BTC"
|
||||
with pytest.raises(RPCException, match="Maximum number of trades is reached."):
|
||||
with pytest.raises(RPCException, match=r"Maximum number of trades is reached\."):
|
||||
rpc._rpc_force_entry(pair, None)
|
||||
freqtradebot.config["max_open_trades"] = 5
|
||||
|
||||
@@ -1286,7 +1286,7 @@ def test_rpc_force_entry_wrong_mode(mocker, default_conf) -> None:
|
||||
patch_get_signal(freqtradebot)
|
||||
rpc = RPC(freqtradebot)
|
||||
pair = "ETH/BTC"
|
||||
with pytest.raises(RPCException, match="Can't go short on Spot markets."):
|
||||
with pytest.raises(RPCException, match=r"Can't go short on Spot markets\."):
|
||||
rpc._rpc_force_entry(pair, None, order_side=SignalDirection.SHORT)
|
||||
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ Unit test file for rpc/api_server.py
|
||||
import asyncio
|
||||
import logging
|
||||
import time
|
||||
from copy import deepcopy
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from pathlib import Path
|
||||
from unittest.mock import ANY, MagicMock, PropertyMock
|
||||
@@ -355,7 +356,7 @@ def test_api__init__(default_conf, mocker):
|
||||
apiserver = ApiServer(default_conf)
|
||||
apiserver.add_rpc_handler(RPC(get_patched_freqtradebot(mocker, default_conf)))
|
||||
assert apiserver._config == default_conf
|
||||
with pytest.raises(OperationalException, match="RPC Handler already attached."):
|
||||
with pytest.raises(OperationalException, match=r"RPC Handler already attached\."):
|
||||
apiserver.add_rpc_handler(RPC(get_patched_freqtradebot(mocker, default_conf)))
|
||||
|
||||
apiserver.cleanup()
|
||||
@@ -534,7 +535,7 @@ def test_api_reloadconf(botclient):
|
||||
|
||||
|
||||
def test_api_pause(botclient):
|
||||
ftbot, client = botclient
|
||||
_ftbot, client = botclient
|
||||
|
||||
rc = client_post(client, f"{BASE_URI}/pause")
|
||||
assert_response(rc)
|
||||
@@ -1860,7 +1861,42 @@ def test_api_forceexit(botclient, mocker, ticker, fee, markets):
|
||||
assert trade.is_open is False
|
||||
|
||||
|
||||
def test_api_pair_candles(botclient, ohlcv_history):
|
||||
def gen_annotation_params():
|
||||
area_annotation = {
|
||||
"type": "area",
|
||||
"start": "2024-01-01 15:00:00",
|
||||
"end": "2024-01-01 16:00:00",
|
||||
"y_start": 94000.2,
|
||||
"y_end": 98000,
|
||||
"color": "",
|
||||
"label": "some label",
|
||||
}
|
||||
line_annotation = {
|
||||
"type": "line",
|
||||
"start": "2024-01-01 15:00:00",
|
||||
"end": "2024-01-01 16:00:00",
|
||||
"y_start": 99000.2,
|
||||
"y_end": 98000,
|
||||
"color": "",
|
||||
"label": "some label",
|
||||
"width": 2,
|
||||
"line_style": "dashed",
|
||||
}
|
||||
|
||||
line_wrong = deepcopy(line_annotation)
|
||||
line_wrong["line_style"] = "dashed2222"
|
||||
return [
|
||||
([area_annotation], [area_annotation]), # Only area
|
||||
([line_annotation], [line_annotation]), # Only line
|
||||
([area_annotation, line_annotation], [area_annotation, line_annotation]), # Both together
|
||||
([], []), # Empty
|
||||
([line_wrong], []), # Invalid line
|
||||
([area_annotation, line_wrong], [area_annotation]), # Invalid line
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("annotations,expected", gen_annotation_params())
|
||||
def test_api_pair_candles(botclient, ohlcv_history, annotations, expected):
|
||||
ftbot, client = botclient
|
||||
timeframe = "5m"
|
||||
amount = 3
|
||||
@@ -1892,18 +1928,7 @@ def test_api_pair_candles(botclient, ohlcv_history):
|
||||
ohlcv_history["exit_short"] = 0
|
||||
|
||||
ftbot.dataprovider._set_cached_df("XRP/BTC", timeframe, ohlcv_history, CandleType.SPOT)
|
||||
fake_plot_annotations = [
|
||||
{
|
||||
"type": "area",
|
||||
"start": "2024-01-01 15:00:00",
|
||||
"end": "2024-01-01 16:00:00",
|
||||
"y_start": 94000.2,
|
||||
"y_end": 98000,
|
||||
"color": "",
|
||||
"label": "some label",
|
||||
}
|
||||
]
|
||||
plot_annotations_mock = MagicMock(return_value=fake_plot_annotations)
|
||||
plot_annotations_mock = MagicMock(return_value=annotations)
|
||||
ftbot.strategy.plot_annotations = plot_annotations_mock
|
||||
for call in ("get", "post"):
|
||||
plot_annotations_mock.reset_mock()
|
||||
@@ -1936,7 +1961,7 @@ def test_api_pair_candles(botclient, ohlcv_history):
|
||||
assert resp["data_start_ts"] == 1511686200000
|
||||
assert resp["data_stop"] == "2017-11-26 09:00:00+00:00"
|
||||
assert resp["data_stop_ts"] == 1511686800000
|
||||
assert resp["annotations"] == fake_plot_annotations
|
||||
assert resp["annotations"] == expected
|
||||
assert plot_annotations_mock.call_count == 1
|
||||
assert isinstance(resp["columns"], list)
|
||||
base_cols = {
|
||||
@@ -3281,7 +3306,7 @@ def test_api_download_data(botclient, mocker, tmp_path):
|
||||
|
||||
|
||||
def test_api_markets_live(botclient):
|
||||
ftbot, client = botclient
|
||||
_ftbot, client = botclient
|
||||
|
||||
rc = client_get(client, f"{BASE_URI}/markets")
|
||||
assert_response(rc, 200)
|
||||
|
||||
@@ -984,7 +984,7 @@ async def test_telegram_profit_long_short_handle(
|
||||
|
||||
mocker.patch("freqtrade.rpc.rpc.CryptoToFiatConverter._find_price", return_value=1.1)
|
||||
mocker.patch.multiple(EXMS, fetch_ticker=ticker_usdt, get_fee=fee)
|
||||
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf_usdt)
|
||||
telegram, _freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf_usdt)
|
||||
|
||||
# When there are no trades
|
||||
await telegram._profit_long(update=update, context=MagicMock())
|
||||
|
||||
@@ -988,7 +988,7 @@ def test_auto_hyperopt_interface_loadparams(default_conf, mocker, caplog):
|
||||
}
|
||||
|
||||
mocker.patch("freqtrade.strategy.hyper.HyperoptTools.load_params", return_value=expected_result)
|
||||
with pytest.raises(OperationalException, match="Invalid parameter file provided."):
|
||||
with pytest.raises(OperationalException, match=r"Invalid parameter file provided\."):
|
||||
StrategyResolver.load_strategy(default_conf)
|
||||
|
||||
mocker.patch(
|
||||
|
||||
@@ -405,9 +405,10 @@ def test_informative_decorator(mocker, default_conf_usdt, trading_mode):
|
||||
assert inf_pair in strategy.gather_informative_pairs()
|
||||
|
||||
def test_historic_ohlcv(pair, timeframe, candle_type):
|
||||
return data[
|
||||
(pair, timeframe or strategy.timeframe, CandleType.from_string(candle_type))
|
||||
].copy()
|
||||
return data.get(
|
||||
(pair, timeframe or strategy.timeframe, CandleType.from_string(candle_type)),
|
||||
pd.DataFrame(),
|
||||
).copy()
|
||||
|
||||
mocker.patch(
|
||||
"freqtrade.data.dataprovider.DataProvider.historic_ohlcv", side_effect=test_historic_ohlcv
|
||||
@@ -430,3 +431,12 @@ def test_informative_decorator(mocker, default_conf_usdt, trading_mode):
|
||||
for _, dataframe in analyzed.items():
|
||||
for col in expected_columns:
|
||||
assert col in dataframe.columns
|
||||
|
||||
# Test non-available pairs
|
||||
del data[("ETH/BTC", "1h", CandleType.SPOT)]
|
||||
with pytest.raises(
|
||||
ValueError, match=r"Informative dataframe for \(ETH\/BTC, 1h, spot\) is empty.*"
|
||||
):
|
||||
strategy.advise_all_indicators(
|
||||
{p: data[(p, strategy.timeframe, candle_def)] for p in ("XRP/USDT", "LTC/USDT")}
|
||||
)
|
||||
|
||||
@@ -111,7 +111,7 @@ def test_load_strategy_noname(default_conf):
|
||||
default_conf["strategy"] = ""
|
||||
with pytest.raises(
|
||||
OperationalException,
|
||||
match="No strategy set. Please use `--strategy` to specify the strategy class to use.",
|
||||
match=r"No strategy set. Please use `--strategy` to specify the strategy class to use\.",
|
||||
):
|
||||
StrategyResolver.load_strategy(default_conf)
|
||||
|
||||
@@ -169,7 +169,8 @@ def test_strategy_override_minimal_roi(caplog, default_conf):
|
||||
|
||||
assert strategy.minimal_roi[0] == 0.5
|
||||
assert log_has(
|
||||
"Override strategy 'minimal_roi' with value in config file: {'20': 0.1, '0': 0.5}.", caplog
|
||||
"Override strategy 'minimal_roi' with value from the configuration: {'20': 0.1, '0': 0.5}.",
|
||||
caplog,
|
||||
)
|
||||
|
||||
|
||||
@@ -179,7 +180,7 @@ def test_strategy_override_stoploss(caplog, default_conf):
|
||||
strategy = StrategyResolver.load_strategy(default_conf)
|
||||
|
||||
assert strategy.stoploss == -0.5
|
||||
assert log_has("Override strategy 'stoploss' with value in config file: -0.5.", caplog)
|
||||
assert log_has("Override strategy 'stoploss' with value from the configuration: -0.5.", caplog)
|
||||
|
||||
|
||||
def test_strategy_override_max_open_trades(caplog, default_conf):
|
||||
@@ -188,7 +189,9 @@ def test_strategy_override_max_open_trades(caplog, default_conf):
|
||||
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)
|
||||
assert log_has(
|
||||
"Override strategy 'max_open_trades' with value from the configuration: 7.", caplog
|
||||
)
|
||||
|
||||
|
||||
def test_strategy_override_trailing_stop(caplog, default_conf):
|
||||
@@ -198,7 +201,9 @@ def test_strategy_override_trailing_stop(caplog, default_conf):
|
||||
|
||||
assert strategy.trailing_stop
|
||||
assert isinstance(strategy.trailing_stop, bool)
|
||||
assert log_has("Override strategy 'trailing_stop' with value in config file: True.", caplog)
|
||||
assert log_has(
|
||||
"Override strategy 'trailing_stop' with value from the configuration: True.", caplog
|
||||
)
|
||||
|
||||
|
||||
def test_strategy_override_trailing_stop_positive(caplog, default_conf):
|
||||
@@ -214,12 +219,14 @@ def test_strategy_override_trailing_stop_positive(caplog, default_conf):
|
||||
|
||||
assert strategy.trailing_stop_positive == -0.1
|
||||
assert log_has(
|
||||
"Override strategy 'trailing_stop_positive' with value in config file: -0.1.", caplog
|
||||
"Override strategy 'trailing_stop_positive' with value from the configuration: -0.1.",
|
||||
caplog,
|
||||
)
|
||||
|
||||
assert strategy.trailing_stop_positive_offset == -0.2
|
||||
assert log_has(
|
||||
"Override strategy 'trailing_stop_positive' with value in config file: -0.1.", caplog
|
||||
"Override strategy 'trailing_stop_positive' with value from the configuration: -0.1.",
|
||||
caplog,
|
||||
)
|
||||
|
||||
|
||||
@@ -233,7 +240,7 @@ def test_strategy_override_timeframe(caplog, default_conf):
|
||||
|
||||
assert strategy.timeframe == 60
|
||||
assert strategy.stake_currency == "ETH"
|
||||
assert log_has("Override strategy 'timeframe' with value in config file: 60.", caplog)
|
||||
assert log_has("Override strategy 'timeframe' with value from the configuration: 60.", caplog)
|
||||
|
||||
|
||||
def test_strategy_override_process_only_new_candles(caplog, default_conf):
|
||||
@@ -244,7 +251,8 @@ def test_strategy_override_process_only_new_candles(caplog, default_conf):
|
||||
|
||||
assert not strategy.process_only_new_candles
|
||||
assert log_has(
|
||||
"Override strategy 'process_only_new_candles' with value in config file: False.", caplog
|
||||
"Override strategy 'process_only_new_candles' with value from the configuration: False.",
|
||||
caplog,
|
||||
)
|
||||
|
||||
|
||||
@@ -265,7 +273,7 @@ def test_strategy_override_order_types(caplog, default_conf):
|
||||
assert strategy.order_types[method] == order_types[method]
|
||||
|
||||
assert log_has(
|
||||
"Override strategy 'order_types' with value in config file:"
|
||||
"Override strategy 'order_types' with value from the configuration:"
|
||||
" {'entry': 'market', 'exit': 'limit', 'stoploss': 'limit',"
|
||||
" 'stoploss_on_exchange': True}.",
|
||||
caplog,
|
||||
@@ -299,7 +307,7 @@ def test_strategy_override_order_tif(caplog, default_conf):
|
||||
assert strategy.order_time_in_force[method] == order_time_in_force[method]
|
||||
|
||||
assert log_has(
|
||||
"Override strategy 'order_time_in_force' with value in config file:"
|
||||
"Override strategy 'order_time_in_force' with value from the configuration:"
|
||||
" {'entry': 'FOK', 'exit': 'GTC'}.",
|
||||
caplog,
|
||||
)
|
||||
@@ -340,7 +348,9 @@ def test_strategy_override_use_exit_signal(caplog, default_conf):
|
||||
|
||||
assert not strategy.use_exit_signal
|
||||
assert isinstance(strategy.use_exit_signal, bool)
|
||||
assert log_has("Override strategy 'use_exit_signal' with value in config file: False.", caplog)
|
||||
assert log_has(
|
||||
"Override strategy 'use_exit_signal' with value from the configuration: False.", caplog
|
||||
)
|
||||
|
||||
|
||||
def test_strategy_override_use_exit_profit_only(caplog, default_conf):
|
||||
@@ -367,7 +377,9 @@ def test_strategy_override_use_exit_profit_only(caplog, default_conf):
|
||||
|
||||
assert strategy.exit_profit_only
|
||||
assert isinstance(strategy.exit_profit_only, bool)
|
||||
assert log_has("Override strategy 'exit_profit_only' with value in config file: True.", caplog)
|
||||
assert log_has(
|
||||
"Override strategy 'exit_profit_only' with value from the configuration: True.", caplog
|
||||
)
|
||||
|
||||
|
||||
def test_strategy_max_open_trades_infinity_from_strategy(caplog, default_conf):
|
||||
|
||||
@@ -250,7 +250,7 @@ def test_from_recursive_files(testdatadir) -> None:
|
||||
assert "test_pricing2_conf.json" in conf["config_files"][3]
|
||||
|
||||
files = testdatadir / "testconfigs/recursive.json"
|
||||
with pytest.raises(OperationalException, match="Config loop detected."):
|
||||
with pytest.raises(OperationalException, match=r"Config loop detected\."):
|
||||
load_from_files([files])
|
||||
|
||||
|
||||
@@ -672,7 +672,7 @@ def test_validate_max_open_trades(default_conf):
|
||||
default_conf["stake_amount"] = "unlimited"
|
||||
with pytest.raises(
|
||||
OperationalException,
|
||||
match="`max_open_trades` and `stake_amount` cannot both be unlimited.",
|
||||
match=r"`max_open_trades` and `stake_amount` cannot both be unlimited\.",
|
||||
):
|
||||
validate_config_consistency(default_conf)
|
||||
|
||||
@@ -691,14 +691,15 @@ def test_validate_price_side(default_conf):
|
||||
conf["order_types"]["entry"] = "market"
|
||||
with pytest.raises(
|
||||
OperationalException,
|
||||
match='Market entry orders require entry_pricing.price_side = "other".',
|
||||
match=r'Market entry orders require entry_pricing.price_side = "other"\.',
|
||||
):
|
||||
validate_config_consistency(conf)
|
||||
|
||||
conf = deepcopy(default_conf)
|
||||
conf["order_types"]["exit"] = "market"
|
||||
with pytest.raises(
|
||||
OperationalException, match='Market exit orders require exit_pricing.price_side = "other".'
|
||||
OperationalException,
|
||||
match=r'Market exit orders require exit_pricing.price_side = "other"\.',
|
||||
):
|
||||
validate_config_consistency(conf)
|
||||
|
||||
@@ -716,8 +717,8 @@ def test_validate_tsl(default_conf):
|
||||
default_conf["stoploss"] = 0.0
|
||||
with pytest.raises(
|
||||
OperationalException,
|
||||
match="The config stoploss needs to be different "
|
||||
"from 0 to avoid problems with sell orders.",
|
||||
match=r"The config stoploss needs to be different "
|
||||
r"from 0 to avoid problems with sell orders\.",
|
||||
):
|
||||
validate_config_consistency(default_conf)
|
||||
default_conf["stoploss"] = -0.10
|
||||
@@ -767,7 +768,7 @@ def test_validate_whitelist(default_conf):
|
||||
del conf["exchange"]["pair_whitelist"]
|
||||
# Test error case
|
||||
with pytest.raises(
|
||||
OperationalException, match="StaticPairList requires pair_whitelist to be set."
|
||||
OperationalException, match=r"StaticPairList requires pair_whitelist to be set\."
|
||||
):
|
||||
validate_config_consistency(conf)
|
||||
|
||||
@@ -969,7 +970,7 @@ def test__validate_consumers(default_conf, caplog) -> None:
|
||||
conf = deepcopy(default_conf)
|
||||
conf.update({"external_message_consumer": {"enabled": True, "producers": []}})
|
||||
with pytest.raises(
|
||||
OperationalException, match="You must specify at least 1 Producer to connect to."
|
||||
OperationalException, match=r"You must specify at least 1 Producer to connect to\."
|
||||
):
|
||||
validate_config_consistency(conf)
|
||||
|
||||
@@ -996,7 +997,7 @@ def test__validate_consumers(default_conf, caplog) -> None:
|
||||
}
|
||||
)
|
||||
with pytest.raises(
|
||||
OperationalException, match="Producer names must be unique. Duplicate: default"
|
||||
OperationalException, match=r"Producer names must be unique\. Duplicate: default"
|
||||
):
|
||||
validate_config_consistency(conf)
|
||||
|
||||
@@ -1026,7 +1027,7 @@ def test__validate_orderflow(default_conf) -> None:
|
||||
conf["exchange"]["use_public_trades"] = True
|
||||
with pytest.raises(
|
||||
ConfigurationError,
|
||||
match="Orderflow is a required configuration key when using public trades.",
|
||||
match=r"Orderflow is a required configuration key when using public trades\.",
|
||||
):
|
||||
validate_config_consistency(conf)
|
||||
|
||||
@@ -1050,7 +1051,7 @@ def test_validate_edge_removal(default_conf):
|
||||
}
|
||||
with pytest.raises(
|
||||
ConfigurationError,
|
||||
match="Edge is no longer supported and has been removed from Freqtrade with 2025.6.",
|
||||
match=r"Edge is no longer supported and has been removed from Freqtrade with 2025\.6\.",
|
||||
):
|
||||
validate_config_consistency(default_conf)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user