Merge branch 'develop' into pr/x-mass/12315
This commit is contained in:
@@ -198,6 +198,8 @@ def test_list_timeframes(mocker, capsys):
|
||||
"1h": "hour",
|
||||
"1d": "day",
|
||||
}
|
||||
api_mock.options = {}
|
||||
|
||||
patch_exchange(mocker, api_mock=api_mock, exchange="bybit")
|
||||
args = [
|
||||
"list-timeframes",
|
||||
@@ -286,6 +288,52 @@ def test_list_timeframes(mocker, capsys):
|
||||
assert re.search(r"^1h$", captured.out, re.MULTILINE)
|
||||
assert re.search(r"^1d$", captured.out, re.MULTILINE)
|
||||
|
||||
api_mock.options = {
|
||||
"timeframes": {
|
||||
"spot": {"1m": "1m", "5m": "5m", "15m": "15m"},
|
||||
"swap": {"1m": "1m", "15m": "15m", "1h": "1h"},
|
||||
}
|
||||
}
|
||||
|
||||
args = [
|
||||
"list-timeframes",
|
||||
"--exchange",
|
||||
"binance",
|
||||
]
|
||||
start_list_timeframes(get_args(args))
|
||||
captured = capsys.readouterr()
|
||||
assert re.match(
|
||||
"Timeframes available for the exchange `Binance`: 1m, 5m, 15m",
|
||||
captured.out,
|
||||
)
|
||||
|
||||
args = [
|
||||
"list-timeframes",
|
||||
"--exchange",
|
||||
"binance",
|
||||
"--trading-mode",
|
||||
"spot",
|
||||
]
|
||||
start_list_timeframes(get_args(args))
|
||||
captured = capsys.readouterr()
|
||||
assert re.match(
|
||||
"Timeframes available for the exchange `Binance`: 1m, 5m, 15m",
|
||||
captured.out,
|
||||
)
|
||||
args = [
|
||||
"list-timeframes",
|
||||
"--exchange",
|
||||
"binance",
|
||||
"--trading-mode",
|
||||
"futures",
|
||||
]
|
||||
start_list_timeframes(get_args(args))
|
||||
captured = capsys.readouterr()
|
||||
assert re.match(
|
||||
"Timeframes available for the exchange `Binance`: 1m, 15m, 1h",
|
||||
captured.out,
|
||||
)
|
||||
|
||||
|
||||
def test_list_markets(mocker, markets_static, capsys):
|
||||
api_mock = MagicMock()
|
||||
@@ -1319,10 +1367,10 @@ def test_hyperopt_list(mocker, capsys, caplog, tmp_path):
|
||||
" 2/12",
|
||||
" 10/12",
|
||||
"Best result:",
|
||||
"Buy hyperspace params",
|
||||
"Sell hyperspace params",
|
||||
"ROI table",
|
||||
"Stoploss",
|
||||
"Buy parameters",
|
||||
"Sell parameters",
|
||||
"ROI parameters",
|
||||
"Stoploss parameters",
|
||||
]
|
||||
)
|
||||
assert all(
|
||||
@@ -1719,7 +1767,7 @@ def test_start_list_data(testdatadir, capsys):
|
||||
pargs["config"] = None
|
||||
start_list_data(pargs)
|
||||
captured = capsys.readouterr()
|
||||
assert "Found 16 pair / timeframe combinations." in captured.out
|
||||
assert "Found 18 pair / timeframe combinations." in captured.out
|
||||
assert re.search(r".*Pair.*Timeframe.*Type.*\n", captured.out)
|
||||
assert re.search(r"\n.* UNITTEST/BTC .* 1m, 5m, 8m, 30m .* spot |\n", captured.out)
|
||||
|
||||
@@ -1753,10 +1801,10 @@ def test_start_list_data(testdatadir, capsys):
|
||||
start_list_data(pargs)
|
||||
captured = capsys.readouterr()
|
||||
|
||||
assert "Found 6 pair / timeframe combinations." in captured.out
|
||||
assert "Found 5 pair / timeframe combinations." in captured.out
|
||||
assert re.search(r".*Pair.*Timeframe.*Type.*\n", captured.out)
|
||||
assert re.search(r"\n.* XRP/USDT:USDT .* 5m, 1h .* futures |\n", captured.out)
|
||||
assert re.search(r"\n.* XRP/USDT:USDT .* 1h, 8h .* mark |\n", captured.out)
|
||||
assert re.search(r"\n.* XRP/USDT:USDT .* 1h.* mark |\n", captured.out)
|
||||
|
||||
args = [
|
||||
"list-data",
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import subprocess # noqa: S404, RUF100
|
||||
import time
|
||||
|
||||
from tests.conftest import is_arm, is_mac
|
||||
from tests.conftest import is_mac
|
||||
|
||||
|
||||
MAXIMUM_STARTUP_TIME = 0.7 if is_mac() and not is_arm() else 0.5
|
||||
MAXIMUM_STARTUP_TIME = 0.7 if is_mac() else 0.5
|
||||
|
||||
|
||||
def test_startup_time():
|
||||
|
||||
+15
-3
@@ -21,6 +21,7 @@ from freqtrade.exchange import Exchange, timeframe_to_minutes, timeframe_to_seco
|
||||
from freqtrade.freqtradebot import FreqtradeBot
|
||||
from freqtrade.persistence import LocalTrade, Order, Trade, init_db
|
||||
from freqtrade.resolvers import ExchangeResolver
|
||||
from freqtrade.system import set_mp_start_method
|
||||
from freqtrade.util import dt_now, dt_ts
|
||||
from freqtrade.worker import Worker
|
||||
from tests.conftest_trades import (
|
||||
@@ -500,9 +501,20 @@ def patch_gc(mocker) -> None:
|
||||
mocker.patch("freqtrade.main.gc_set_threshold")
|
||||
|
||||
|
||||
def is_arm() -> bool:
|
||||
@pytest.fixture(scope="session", autouse=True)
|
||||
def fixture_set_mp_start_method():
|
||||
"""
|
||||
Patch multiprocessing start mode globally
|
||||
Auto-used, runs once per session.
|
||||
"""
|
||||
set_mp_start_method()
|
||||
|
||||
|
||||
def is_arm(include_aarch64: bool = False) -> bool:
|
||||
machine = platform.machine()
|
||||
return "arm" in machine or "aarch64" in machine
|
||||
if include_aarch64:
|
||||
return "aarch64" in machine or "arm" in machine
|
||||
return "arm" in machine
|
||||
|
||||
|
||||
def is_mac() -> bool:
|
||||
@@ -3172,7 +3184,7 @@ def leverage_tiers():
|
||||
},
|
||||
{
|
||||
"minNotional": 5000000,
|
||||
"maxNotional": 30000000,
|
||||
"maxNotional": None,
|
||||
"maintenanceMarginRate": 0.5,
|
||||
"maxLeverage": 1,
|
||||
"maintAmt": 1527500.0,
|
||||
|
||||
@@ -303,6 +303,7 @@ def mock_order_usdt_6(is_short: bool):
|
||||
"side": entry_side(is_short),
|
||||
"type": "limit",
|
||||
"price": 10.0,
|
||||
"cost": 20.0,
|
||||
"amount": 2.0,
|
||||
"filled": 2.0,
|
||||
"remaining": 0.0,
|
||||
@@ -317,6 +318,7 @@ def mock_order_usdt_6_exit(is_short: bool):
|
||||
"side": exit_side(is_short),
|
||||
"type": "limit",
|
||||
"price": 12.0,
|
||||
"cost": 24.0,
|
||||
"amount": 2.0,
|
||||
"filled": 0.0,
|
||||
"remaining": 2.0,
|
||||
|
||||
@@ -290,20 +290,23 @@ def test_combine_dataframes_with_mean(testdatadir):
|
||||
|
||||
|
||||
def test_combined_dataframes_with_rel_mean(testdatadir):
|
||||
pairs = ["ETH/BTC", "ADA/BTC"]
|
||||
pairs = ["BTC/USDT", "XRP/USDT"]
|
||||
data = load_data(datadir=testdatadir, pairs=pairs, timeframe="5m")
|
||||
df = combined_dataframes_with_rel_mean(
|
||||
data, datetime(2018, 1, 12, tzinfo=UTC), datetime(2018, 1, 28, tzinfo=UTC)
|
||||
data,
|
||||
fromdt=data["BTC/USDT"].at[0, "date"],
|
||||
todt=data["BTC/USDT"].at[data["BTC/USDT"].index[-1], "date"],
|
||||
)
|
||||
assert isinstance(df, DataFrame)
|
||||
assert "ETH/BTC" not in df.columns
|
||||
assert "ADA/BTC" not in df.columns
|
||||
assert "BTC/USDT" not in df.columns
|
||||
assert "XRP/USDT" not in df.columns
|
||||
assert "mean" in df.columns
|
||||
assert "rel_mean" in df.columns
|
||||
assert "count" in df.columns
|
||||
assert df.iloc[0]["count"] == 2
|
||||
assert df.iloc[-1]["count"] == 2
|
||||
assert len(df) < len(data["ETH/BTC"])
|
||||
assert len(df) < len(data["BTC/USDT"])
|
||||
assert df["rel_mean"].between(-0.5, 0.5).all()
|
||||
|
||||
|
||||
def test_combine_dataframes_with_mean_no_data(testdatadir):
|
||||
@@ -575,12 +578,18 @@ def test_calculate_max_drawdown2():
|
||||
# No losing trade ...
|
||||
drawdown = calculate_max_drawdown(df, date_col="open_date", value_col="profit")
|
||||
assert drawdown.drawdown_abs == 0.0
|
||||
assert drawdown.low_value == 0.0
|
||||
assert drawdown.current_high_value >= 0.0
|
||||
assert drawdown.current_drawdown_abs == 0.0
|
||||
|
||||
df1 = DataFrame(zip(values[:5], dates[:5], strict=False), columns=["profit", "open_date"])
|
||||
df1.loc[:, "profit"] = df1["profit"] * -1
|
||||
# No winning trade ...
|
||||
drawdown = calculate_max_drawdown(df1, date_col="open_date", value_col="profit")
|
||||
assert drawdown.drawdown_abs == 0.055545
|
||||
assert drawdown.high_value == 0.0
|
||||
assert drawdown.current_high_value == 0.0
|
||||
assert drawdown.current_drawdown_abs == 0.055545
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
@@ -39,12 +39,6 @@ def populate_dataframe_with_trades_trades(testdatadir):
|
||||
return pd.read_feather(testdatadir / "orderflow/populate_dataframe_with_trades_TRADES.feather")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def candles(testdatadir):
|
||||
# TODO: this fixture isn't really necessary and could be removed
|
||||
return pd.read_json(testdatadir / "orderflow/candles.json").copy()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def public_trades_list(testdatadir):
|
||||
return read_csv(testdatadir / "orderflow/public_trades_list.csv").copy()
|
||||
@@ -293,7 +287,7 @@ def test_public_trades_trades_mock_populate_dataframe_with_trades__check_trades(
|
||||
assert t["price"] == 234.72
|
||||
|
||||
|
||||
def test_public_trades_put_volume_profile_into_ohlcv_candles(public_trades_list_simple, candles):
|
||||
def test_public_trades_put_volume_profile_into_ohlcv_candles(public_trades_list_simple):
|
||||
"""
|
||||
Tests the integration of volume profile data into OHLCV candles.
|
||||
|
||||
@@ -412,13 +406,11 @@ def test_public_trades_config_max_trades(
|
||||
|
||||
|
||||
def test_public_trades_testdata_sanity(
|
||||
candles,
|
||||
public_trades_list,
|
||||
public_trades_list_simple,
|
||||
populate_dataframe_with_trades_dataframe,
|
||||
populate_dataframe_with_trades_trades,
|
||||
):
|
||||
assert 10999 == len(candles)
|
||||
assert 1000 == len(public_trades_list)
|
||||
assert 999 == len(populate_dataframe_with_trades_dataframe)
|
||||
assert 293532 == len(populate_dataframe_with_trades_trades)
|
||||
|
||||
@@ -40,6 +40,8 @@ def test_datahandler_ohlcv_get_pairs(testdatadir):
|
||||
"NXT/BTC",
|
||||
"DASH/BTC",
|
||||
"XRP/ETH",
|
||||
"BTC/USDT",
|
||||
"XRP/USDT",
|
||||
}
|
||||
|
||||
pairs = JsonGzDataHandler.ohlcv_get_pairs(testdatadir, "8m", candle_type=CandleType.SPOT)
|
||||
@@ -111,6 +113,8 @@ def test_datahandler_ohlcv_get_available_data(testdatadir):
|
||||
("DASH/BTC", "5m", CandleType.SPOT),
|
||||
("XRP/ETH", "1m", CandleType.SPOT),
|
||||
("XRP/ETH", "5m", CandleType.SPOT),
|
||||
("BTC/USDT", "5m", CandleType.SPOT),
|
||||
("XRP/USDT", "5m", CandleType.SPOT),
|
||||
("UNITTEST/BTC", "30m", CandleType.SPOT),
|
||||
("UNITTEST/BTC", "8m", CandleType.SPOT),
|
||||
}
|
||||
@@ -122,8 +126,7 @@ def test_datahandler_ohlcv_get_available_data(testdatadir):
|
||||
("XRP/USDT:USDT", "5m", "futures"),
|
||||
("XRP/USDT:USDT", "1h", "futures"),
|
||||
("XRP/USDT:USDT", "1h", "mark"),
|
||||
("XRP/USDT:USDT", "8h", "mark"),
|
||||
("XRP/USDT:USDT", "8h", "funding_rate"),
|
||||
("XRP/USDT:USDT", "1h", "funding_rate"),
|
||||
}
|
||||
|
||||
paircombs = JsonGzDataHandler.ohlcv_get_available_data(testdatadir, TradingMode.SPOT)
|
||||
@@ -285,7 +288,7 @@ def test_jsondatahandler_trades_load(testdatadir, caplog):
|
||||
dh.trades_load("XRP/ETH", TradingMode.SPOT)
|
||||
assert not log_has(logmsg, caplog)
|
||||
|
||||
# Test conversation is happening
|
||||
# Test conversion is happening
|
||||
dh.trades_load("XRP/OLD", TradingMode.SPOT)
|
||||
assert log_has(logmsg, caplog)
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ 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
|
||||
from tests.conftest import EXMS, generate_test_data, get_patched_exchange, log_has_re
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -185,6 +185,28 @@ def test_get_pair_dataframe(mocker, default_conf, ohlcv_history, candle_type):
|
||||
assert len(df) == 2 # ohlcv_history is limited to 2 rows now
|
||||
|
||||
|
||||
def test_get_pair_dataframe_funding_rate(mocker, default_conf, ohlcv_history, caplog):
|
||||
default_conf["runmode"] = RunMode.DRY_RUN
|
||||
timeframe = "1h"
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
candletype = CandleType.FUNDING_RATE
|
||||
exchange._klines[("XRP/BTC", timeframe, candletype)] = ohlcv_history
|
||||
exchange._klines[("UNITTEST/BTC", timeframe, candletype)] = ohlcv_history
|
||||
|
||||
dp = DataProvider(default_conf, exchange)
|
||||
assert dp.runmode == RunMode.DRY_RUN
|
||||
assert ohlcv_history.equals(
|
||||
dp.get_pair_dataframe("UNITTEST/BTC", timeframe, candle_type="funding_rate")
|
||||
)
|
||||
msg = r".*funding rate timeframe not matching"
|
||||
assert not log_has_re(msg, caplog)
|
||||
|
||||
assert ohlcv_history.equals(
|
||||
dp.get_pair_dataframe("UNITTEST/BTC", "5h", candle_type="funding_rate")
|
||||
)
|
||||
assert log_has_re(msg, caplog)
|
||||
|
||||
|
||||
def test_available_pairs(mocker, default_conf, ohlcv_history):
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
timeframe = default_conf["timeframe"]
|
||||
@@ -636,3 +658,21 @@ def test_check_delisting(mocker, default_conf_usdt):
|
||||
assert res == dt_utc(2025, 10, 2)
|
||||
|
||||
assert delist_mock2.call_count == 1
|
||||
|
||||
|
||||
def test_get_funding_rate_timeframe(mocker, default_conf_usdt):
|
||||
default_conf_usdt["trading_mode"] = "futures"
|
||||
default_conf_usdt["margin_mode"] = "isolated"
|
||||
exchange = get_patched_exchange(mocker, default_conf_usdt)
|
||||
mock_get_option = mocker.spy(exchange, "get_option")
|
||||
dp = DataProvider(default_conf_usdt, exchange)
|
||||
|
||||
assert dp.get_funding_rate_timeframe() == "1h"
|
||||
mock_get_option.assert_called_once_with("funding_fee_timeframe")
|
||||
|
||||
|
||||
def test_get_funding_rate_timeframe_no_exchange(default_conf_usdt):
|
||||
dp = DataProvider(default_conf_usdt, None)
|
||||
|
||||
with pytest.raises(OperationalException, match=r"Exchange is not available to DataProvider."):
|
||||
dp.get_funding_rate_timeframe()
|
||||
|
||||
+49
-11
@@ -534,18 +534,19 @@ def test_validate_backtest_data(default_conf, mocker, caplog, testdatadir) -> No
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"trademode,callcount",
|
||||
"trademode,callcount, callcount_parallel",
|
||||
[
|
||||
("spot", 4),
|
||||
("margin", 4),
|
||||
("futures", 8), # Called 8 times - 4 normal, 2 funding and 2 mark/index calls
|
||||
("spot", 4, 2),
|
||||
("margin", 4, 2),
|
||||
("futures", 8, 4), # Called 8 times - 4 normal, 2 funding and 2 mark/index calls
|
||||
],
|
||||
)
|
||||
def test_refresh_backtest_ohlcv_data(
|
||||
mocker, default_conf, markets, caplog, testdatadir, trademode, callcount
|
||||
mocker, default_conf, markets, caplog, testdatadir, trademode, callcount, callcount_parallel
|
||||
):
|
||||
caplog.set_level(logging.DEBUG)
|
||||
dl_mock = mocker.patch("freqtrade.data.history.history_utils._download_pair_history")
|
||||
mocker.patch(f"{EXMS}.verify_candle_type_support", MagicMock())
|
||||
|
||||
def parallel_mock(pairs, timeframe, candle_type, **kwargs):
|
||||
return {(pair, timeframe, candle_type): DataFrame() for pair in pairs}
|
||||
@@ -573,14 +574,50 @@ def test_refresh_backtest_ohlcv_data(
|
||||
)
|
||||
|
||||
# Called once per timeframe (as we return an empty dataframe)
|
||||
assert parallel_mock.call_count == 2
|
||||
# called twice for spot/margin and 4 times for futures
|
||||
assert parallel_mock.call_count == callcount_parallel
|
||||
assert dl_mock.call_count == callcount
|
||||
assert dl_mock.call_args[1]["timerange"].starttype == "date"
|
||||
|
||||
assert log_has_re(r"Downloading pair ETH/BTC, .* interval 1m\.", caplog)
|
||||
if trademode == "futures":
|
||||
assert log_has_re(r"Downloading pair ETH/BTC, funding_rate, interval 8h\.", caplog)
|
||||
assert log_has_re(r"Downloading pair ETH/BTC, mark, interval 4h\.", caplog)
|
||||
assert log_has_re(r"Downloading pair ETH/BTC, funding_rate, interval 1h\.", caplog)
|
||||
assert log_has_re(r"Downloading pair ETH/BTC, mark, interval 1h\.", caplog)
|
||||
|
||||
# Test with only one pair - no parallel download should happen 1 pair/timeframe combination
|
||||
# doesn't justify parallelization
|
||||
parallel_mock.reset_mock()
|
||||
dl_mock.reset_mock()
|
||||
refresh_backtest_ohlcv_data(
|
||||
exchange=ex,
|
||||
pairs=[
|
||||
"ETH/BTC",
|
||||
],
|
||||
timeframes=["5m"],
|
||||
datadir=testdatadir,
|
||||
timerange=timerange,
|
||||
erase=False,
|
||||
trading_mode=trademode,
|
||||
)
|
||||
assert parallel_mock.call_count == 0
|
||||
|
||||
if trademode == "futures":
|
||||
dl_mock.reset_mock()
|
||||
refresh_backtest_ohlcv_data(
|
||||
exchange=ex,
|
||||
pairs=[
|
||||
"ETH/BTC",
|
||||
],
|
||||
timeframes=["5m", "1h"],
|
||||
datadir=testdatadir,
|
||||
timerange=timerange,
|
||||
erase=False,
|
||||
trading_mode=trademode,
|
||||
no_parallel_download=True,
|
||||
candle_types=["premiumIndex", "funding_rate"],
|
||||
)
|
||||
assert parallel_mock.call_count == 0
|
||||
assert dl_mock.call_count == 3 # 2 timeframes premiumIndex + 1x funding_rate
|
||||
|
||||
|
||||
def test_download_data_no_markets(mocker, default_conf, caplog, testdatadir):
|
||||
@@ -780,6 +817,7 @@ def test_download_all_pairs_history_parallel(mocker, default_conf_usdt):
|
||||
exchange.refresh_latest_ohlcv.reset_mock()
|
||||
|
||||
# Test without timerange
|
||||
# expected to call refresh_latest_ohlcv - as we can't know how much will be required.
|
||||
result3 = _download_all_pairs_history_parallel(
|
||||
exchange=exchange,
|
||||
pairs=pairs,
|
||||
@@ -787,8 +825,8 @@ def test_download_all_pairs_history_parallel(mocker, default_conf_usdt):
|
||||
candle_type=candle_type,
|
||||
timerange=None,
|
||||
)
|
||||
assert result3 == {}
|
||||
assert exchange.refresh_latest_ohlcv.call_count == 0
|
||||
assert result3 == expected
|
||||
assert exchange.refresh_latest_ohlcv.call_count == 1
|
||||
|
||||
|
||||
def test_download_pair_history_with_pair_candles(mocker, default_conf, tmp_path, caplog) -> None:
|
||||
@@ -878,7 +916,7 @@ def test_download_pair_history_with_pair_candles(mocker, default_conf, tmp_path,
|
||||
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)
|
||||
assert log_has("Downloaded data for TEST/BTC, 5m, spot with length 3. Parallel Method.", caplog)
|
||||
|
||||
# Verify data was stored
|
||||
assert data_handler_mock.ohlcv_store.call_count == 1
|
||||
|
||||
@@ -157,7 +157,8 @@ def test_create_stoploss_order_dry_run_binance(default_conf, mocker):
|
||||
assert "type" in order
|
||||
|
||||
assert order["type"] == order_type
|
||||
assert order["price"] == 220
|
||||
assert order["price"] == 217.8
|
||||
assert order["stopPrice"] == 220
|
||||
assert order["amount"] == 1
|
||||
|
||||
|
||||
@@ -974,6 +975,18 @@ def test_get_historic_ohlcv_binance(
|
||||
archive_mock.assert_called_once()
|
||||
if api_called:
|
||||
api_mock.assert_called_once()
|
||||
candle_mock.reset_mock()
|
||||
api_mock.reset_mock()
|
||||
archive_mock.reset_mock()
|
||||
|
||||
# binanceus does not use archive mode!
|
||||
exchange._can_use_data_download_fast = False
|
||||
df = exchange.get_historic_ohlcv(pair, timeframe, since_ms, candle_type, is_new_pair, until_ms)
|
||||
# Never uses archive
|
||||
assert archive_mock.call_count == 0
|
||||
assert candle_mock.call_count == (0 if not candle_called else 1)
|
||||
if api_called:
|
||||
assert api_mock.call_count == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
from copy import deepcopy
|
||||
from datetime import timedelta
|
||||
from unittest.mock import MagicMock
|
||||
from unittest.mock import MagicMock, PropertyMock
|
||||
|
||||
import pytest
|
||||
|
||||
from freqtrade.enums import CandleType
|
||||
from freqtrade.exceptions import RetryableOrderError
|
||||
from freqtrade.enums import CandleType, MarginMode, RunMode, TradingMode
|
||||
from freqtrade.exceptions import OperationalException, RetryableOrderError
|
||||
from freqtrade.exchange.common import API_RETRY_COUNT
|
||||
from freqtrade.util import dt_now, dt_ts
|
||||
from freqtrade.util import dt_now, dt_ts, dt_utc
|
||||
from tests.conftest import EXMS, get_patched_exchange
|
||||
from tests.exchange.test_exchange import ccxt_exceptionhandlers
|
||||
|
||||
@@ -120,3 +121,116 @@ def test_bitget_ohlcv_candle_limit(mocker, default_conf_usdt):
|
||||
assert exch.ohlcv_candle_limit(timeframe, CandleType.FUTURES, start_time) == length
|
||||
assert exch.ohlcv_candle_limit(timeframe, CandleType.MARK, start_time) == length
|
||||
assert exch.ohlcv_candle_limit(timeframe, CandleType.FUNDING_RATE, start_time) == 200
|
||||
|
||||
|
||||
def test_additional_exchange_init_bitget(default_conf, mocker):
|
||||
default_conf["dry_run"] = False
|
||||
default_conf["trading_mode"] = TradingMode.FUTURES
|
||||
default_conf["margin_mode"] = MarginMode.ISOLATED
|
||||
api_mock = MagicMock()
|
||||
api_mock.set_position_mode = MagicMock(return_value={})
|
||||
|
||||
get_patched_exchange(mocker, default_conf, exchange="bitget", api_mock=api_mock)
|
||||
assert api_mock.set_position_mode.call_count == 1
|
||||
|
||||
ccxt_exceptionhandlers(
|
||||
mocker, default_conf, api_mock, "bitget", "additional_exchange_init", "set_position_mode"
|
||||
)
|
||||
|
||||
|
||||
def test_dry_run_liquidation_price_cross_bitget(default_conf, mocker):
|
||||
default_conf["dry_run"] = True
|
||||
default_conf["trading_mode"] = TradingMode.FUTURES
|
||||
default_conf["margin_mode"] = MarginMode.CROSS
|
||||
api_mock = MagicMock()
|
||||
mocker.patch(f"{EXMS}.get_maintenance_ratio_and_amt", MagicMock(return_value=(0.005, 0.0)))
|
||||
exchange = get_patched_exchange(mocker, default_conf, exchange="bitget", api_mock=api_mock)
|
||||
|
||||
with pytest.raises(
|
||||
OperationalException, match="Freqtrade currently only supports isolated futures for bitget"
|
||||
):
|
||||
exchange.dry_run_liquidation_price(
|
||||
"ETH/USDT:USDT",
|
||||
100_000,
|
||||
False,
|
||||
0.1,
|
||||
100,
|
||||
10,
|
||||
100,
|
||||
[],
|
||||
)
|
||||
|
||||
|
||||
def test__lev_prep_bitget(default_conf, mocker):
|
||||
api_mock = MagicMock()
|
||||
api_mock.set_margin_mode = MagicMock()
|
||||
api_mock.set_leverage = MagicMock()
|
||||
type(api_mock).has = PropertyMock(return_value={"setMarginMode": True, "setLeverage": True})
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange="bitget")
|
||||
exchange._lev_prep("BTC/USDC:USDC", 3.2, "buy")
|
||||
|
||||
assert api_mock.set_margin_mode.call_count == 0
|
||||
assert api_mock.set_leverage.call_count == 0
|
||||
|
||||
# test in futures mode
|
||||
api_mock.set_margin_mode.reset_mock()
|
||||
api_mock.set_leverage.reset_mock()
|
||||
default_conf["dry_run"] = False
|
||||
|
||||
default_conf["trading_mode"] = "futures"
|
||||
default_conf["margin_mode"] = "isolated"
|
||||
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange="bitget")
|
||||
exchange._lev_prep("BTC/USDC:USDC", 3.2, "buy")
|
||||
|
||||
assert api_mock.set_margin_mode.call_count == 0
|
||||
assert api_mock.set_leverage.call_count == 1
|
||||
api_mock.set_leverage.assert_called_with(symbol="BTC/USDC:USDC", leverage=3.2)
|
||||
|
||||
api_mock.reset_mock()
|
||||
|
||||
exchange._lev_prep("BTC/USDC:USDC", 19.99, "sell")
|
||||
|
||||
assert api_mock.set_margin_mode.call_count == 0
|
||||
assert api_mock.set_leverage.call_count == 1
|
||||
api_mock.set_leverage.assert_called_with(symbol="BTC/USDC:USDC", leverage=19.99)
|
||||
|
||||
|
||||
def test_check_delisting_time_bitget(default_conf_usdt, mocker):
|
||||
exchange = get_patched_exchange(mocker, default_conf_usdt, exchange="bitget")
|
||||
exchange._config["runmode"] = RunMode.BACKTEST
|
||||
delist_fut_mock = MagicMock(return_value=None)
|
||||
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_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_fut_mock.call_count == 0
|
||||
|
||||
# Delist futures called
|
||||
exchange.trading_mode = TradingMode.FUTURES
|
||||
resp1 = exchange.check_delisting_time("BTC/USDT:USDT")
|
||||
assert resp1 is None
|
||||
assert delist_fut_mock.call_count == 1
|
||||
|
||||
|
||||
def test__check_delisting_futures_bitget(default_conf_usdt, mocker, markets):
|
||||
markets["BTC/USDT:USDT"] = deepcopy(markets["SOL/BUSD:BUSD"])
|
||||
markets["BTC/USDT:USDT"]["info"]["limitOpenTime"] = "-1"
|
||||
markets["SOL/BUSD:BUSD"]["info"]["limitOpenTime"] = "-1"
|
||||
markets["ADA/USDT:USDT"]["info"]["limitOpenTime"] = "1760745600000" # 2025-10-18
|
||||
exchange = get_patched_exchange(mocker, default_conf_usdt, exchange="bitget")
|
||||
mocker.patch(f"{EXMS}.markets", PropertyMock(return_value=markets))
|
||||
|
||||
resp_sol = exchange._check_delisting_futures("SOL/BUSD:BUSD")
|
||||
# No delisting date
|
||||
assert resp_sol is None
|
||||
# Has a delisting date
|
||||
resp_ada = exchange._check_delisting_futures("ADA/USDT:USDT")
|
||||
assert resp_ada == dt_utc(2025, 10, 18)
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
from copy import deepcopy
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from unittest.mock import MagicMock
|
||||
from unittest.mock import MagicMock, PropertyMock
|
||||
|
||||
import pytest
|
||||
|
||||
from freqtrade.enums.marginmode import MarginMode
|
||||
from freqtrade.enums.tradingmode import TradingMode
|
||||
from freqtrade.enums import MarginMode, RunMode, TradingMode
|
||||
from freqtrade.util import dt_utc
|
||||
from tests.conftest import EXMS, get_mock_coro, get_patched_exchange, log_has
|
||||
from tests.exchange.test_exchange import ccxt_exceptionhandlers
|
||||
|
||||
@@ -214,3 +215,43 @@ def test_bybit__order_needs_price(
|
||||
exchange.unified_account = uta
|
||||
|
||||
assert exchange._order_needs_price(side, order_type) == expected
|
||||
|
||||
|
||||
def test_check_delisting_time_bybit(default_conf_usdt, mocker):
|
||||
exchange = get_patched_exchange(mocker, default_conf_usdt, exchange="bybit")
|
||||
exchange._config["runmode"] = RunMode.BACKTEST
|
||||
delist_fut_mock = MagicMock(return_value=None)
|
||||
mocker.patch.object(exchange, "_check_delisting_futures", delist_fut_mock)
|
||||
|
||||
# Invalid run mode
|
||||
resp = exchange.check_delisting_time("BTC/USDT:USDT")
|
||||
assert resp is None
|
||||
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_fut_mock.call_count == 0
|
||||
|
||||
# Delist futures called
|
||||
exchange.trading_mode = TradingMode.FUTURES
|
||||
resp1 = exchange.check_delisting_time("BTC/USDT:USDT")
|
||||
assert resp1 is None
|
||||
assert delist_fut_mock.call_count == 1
|
||||
|
||||
|
||||
def test__check_delisting_futures_bybit(default_conf_usdt, mocker, markets):
|
||||
markets["BTC/USDT:USDT"] = deepcopy(markets["SOL/BUSD:BUSD"])
|
||||
markets["BTC/USDT:USDT"]["info"]["deliveryTime"] = "0"
|
||||
markets["SOL/BUSD:BUSD"]["info"]["deliveryTime"] = "0"
|
||||
markets["ADA/USDT:USDT"]["info"]["deliveryTime"] = "1760745600000" # 2025-10-18
|
||||
exchange = get_patched_exchange(mocker, default_conf_usdt, exchange="bybit")
|
||||
mocker.patch(f"{EXMS}.markets", PropertyMock(return_value=markets))
|
||||
|
||||
resp_sol = exchange._check_delisting_futures("SOL/BUSD:BUSD")
|
||||
# SOL has no delisting date
|
||||
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)
|
||||
|
||||
+404
-84
@@ -170,7 +170,7 @@ def test_init(default_conf, mocker, caplog):
|
||||
def test_init_ccxt_kwargs(default_conf, mocker, caplog):
|
||||
mocker.patch(f"{EXMS}.reload_markets")
|
||||
mocker.patch(f"{EXMS}.validate_stakecurrency")
|
||||
aei_mock = mocker.patch(f"{EXMS}.additional_exchange_init")
|
||||
aei_mock = mocker.patch(f"{EXMS}.ft_additional_exchange_init")
|
||||
|
||||
caplog.set_level(logging.INFO)
|
||||
conf = copy.deepcopy(default_conf)
|
||||
@@ -742,10 +742,11 @@ def test_get_pair_base_currency(default_conf, mocker, pair, expected):
|
||||
def test_validate_timeframes(default_conf, mocker, timeframe):
|
||||
default_conf["timeframe"] = timeframe
|
||||
api_mock = MagicMock()
|
||||
id_mock = PropertyMock(return_value="test_exchange")
|
||||
type(api_mock).id = id_mock
|
||||
timeframes = PropertyMock(return_value={"1m": "1m", "5m": "5m", "15m": "15m", "1h": "1h"})
|
||||
type(api_mock).timeframes = timeframes
|
||||
id_mock = MagicMock(return_value="test_exchange")
|
||||
api_mock.id = id_mock
|
||||
api_mock.options = {}
|
||||
timeframes = {"1m": "1m", "5m": "5m", "15m": "15m", "1h": "1h"}
|
||||
api_mock.timeframes = timeframes
|
||||
|
||||
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
|
||||
mocker.patch(f"{EXMS}.reload_markets")
|
||||
@@ -757,12 +758,11 @@ def test_validate_timeframes(default_conf, mocker, timeframe):
|
||||
def test_validate_timeframes_failed(default_conf, mocker):
|
||||
default_conf["timeframe"] = "3m"
|
||||
api_mock = MagicMock()
|
||||
id_mock = PropertyMock(return_value="test_exchange")
|
||||
type(api_mock).id = id_mock
|
||||
timeframes = PropertyMock(
|
||||
return_value={"15s": "15s", "1m": "1m", "5m": "5m", "15m": "15m", "1h": "1h"}
|
||||
)
|
||||
type(api_mock).timeframes = timeframes
|
||||
id_mock = MagicMock(return_value="test_exchange")
|
||||
api_mock.id = id_mock
|
||||
timeframes = {"15s": "15s", "1m": "1m", "5m": "5m", "15m": "15m", "1h": "1h"}
|
||||
api_mock.timeframes = timeframes
|
||||
api_mock.options = {}
|
||||
|
||||
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
|
||||
mocker.patch(f"{EXMS}.reload_markets")
|
||||
@@ -1012,7 +1012,7 @@ def test_validate_required_startup_candles(default_conf, mocker, caplog):
|
||||
ex._ft_has["ohlcv_has_history"] = False
|
||||
with pytest.raises(
|
||||
OperationalException,
|
||||
match=r"This strategy requires 2500.*, " r"which is more than the amount.*",
|
||||
match=r"This strategy requires 2500.*, " r"which is more than .* the amount",
|
||||
):
|
||||
ex.validate_required_startup_candles(2500, "5m")
|
||||
|
||||
@@ -1110,6 +1110,191 @@ def test_create_dry_run_order_fees(
|
||||
assert order1["fee"]["rate"] == fee
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"side,limit,offset,is_stop,expected",
|
||||
[
|
||||
("buy", 46.0, 0.0, False, True),
|
||||
("buy", 46.0, 0.0, True, False),
|
||||
("buy", 26.0, 0.0, False, True),
|
||||
("buy", 26.0, 0.0, True, False), # Stop - didn't trigger
|
||||
("buy", 25.55, 0.0, False, False),
|
||||
("buy", 25.55, 0.0, True, True), # Stop - triggered
|
||||
("buy", 1, 0.0, False, False), # Very far away
|
||||
("buy", 1, 0.0, True, True), # Current price is above stop - triggered
|
||||
("sell", 25.5, 0.0, False, True),
|
||||
("sell", 50, 0.0, False, False), # Very far away
|
||||
("sell", 25.58, 0.0, False, False),
|
||||
("sell", 25.563, 0.01, False, False),
|
||||
("sell", 25.563, 0.0, True, False), # stop order - Not triggered, best bid
|
||||
("sell", 25.566, 0.0, True, True), # stop order - triggered
|
||||
("sell", 26, 0.01, True, True), # stop order - triggered
|
||||
("sell", 5.563, 0.01, False, True),
|
||||
("sell", 5.563, 0.0, True, False), # stop order - not triggered
|
||||
],
|
||||
)
|
||||
def test__dry_is_price_crossed_with_orderbook(
|
||||
default_conf, mocker, order_book_l2_usd, side, limit, offset, is_stop, expected
|
||||
):
|
||||
# Best bid 25.563
|
||||
# Best ask 25.566
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
mocker.patch(f"{EXMS}.exchange_has", return_value=True)
|
||||
exchange.fetch_l2_order_book = order_book_l2_usd
|
||||
orderbook = order_book_l2_usd.return_value
|
||||
result = exchange._dry_is_price_crossed(
|
||||
"LTC/USDT", side, limit, orderbook=orderbook, offset=offset, is_stop=is_stop
|
||||
)
|
||||
assert result is expected
|
||||
assert order_book_l2_usd.call_count == 0
|
||||
|
||||
# Test without passing orderbook
|
||||
order_book_l2_usd.reset_mock()
|
||||
result = exchange._dry_is_price_crossed("LTC/USDT", side, limit, offset=offset, is_stop=is_stop)
|
||||
assert result is expected
|
||||
|
||||
|
||||
def test__dry_is_price_crossed_empty_orderbook(default_conf, mocker):
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
mocker.patch(f"{EXMS}.exchange_has", return_value=True)
|
||||
empty_book = {"asks": [], "bids": []}
|
||||
assert not exchange._dry_is_price_crossed("LTC/USDT", "buy", 100.0, orderbook=empty_book)
|
||||
|
||||
|
||||
def test__dry_is_price_crossed_fetches_orderbook(default_conf, mocker, order_book_l2_usd):
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
mocker.patch(f"{EXMS}.exchange_has", return_value=True)
|
||||
exchange.fetch_l2_order_book = order_book_l2_usd
|
||||
assert exchange._dry_is_price_crossed("LTC/USDT", "buy", 26.0)
|
||||
assert order_book_l2_usd.call_count == 1
|
||||
|
||||
|
||||
def test__dry_is_price_crossed_without_orderbook_support(default_conf, mocker):
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
exchange.fetch_l2_order_book = MagicMock()
|
||||
mocker.patch(f"{EXMS}.exchange_has", return_value=False)
|
||||
assert exchange._dry_is_price_crossed("LTC/USDT", "buy", 1.0)
|
||||
assert exchange._dry_is_price_crossed("LTC/USDT", "sell", 1.0)
|
||||
assert exchange.fetch_l2_order_book.call_count == 0
|
||||
assert not exchange._dry_is_price_crossed("LTC/USDT", "buy", 1.0, is_stop=True)
|
||||
assert not exchange._dry_is_price_crossed("LTC/USDT", "sell", 1.0, is_stop=True)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"crossed,immediate,side,amount,expected_status,expected_fee_rate,expected_calls,taker_or_maker",
|
||||
[
|
||||
(True, True, "buy", 2.0, "closed", 0.005, 1, "taker"),
|
||||
(True, False, "sell", 1.5, "closed", 0.005, 1, "maker"),
|
||||
(False, False, "sell", 1.0, "open", None, 0, None),
|
||||
],
|
||||
)
|
||||
def test_check_dry_limit_order_filled(
|
||||
default_conf,
|
||||
mocker,
|
||||
crossed,
|
||||
immediate,
|
||||
side,
|
||||
amount,
|
||||
expected_status,
|
||||
expected_fee_rate,
|
||||
expected_calls,
|
||||
taker_or_maker,
|
||||
):
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
mocker.patch(f"{EXMS}._dry_is_price_crossed", return_value=crossed)
|
||||
fee_mock = mocker.patch(f"{EXMS}.get_fee", return_value=0.005)
|
||||
|
||||
order = {
|
||||
"symbol": "LTC/USDT",
|
||||
"status": "open",
|
||||
"type": "limit",
|
||||
"side": side,
|
||||
"price": 25.0,
|
||||
"amount": amount,
|
||||
"filled": 0.0,
|
||||
"remaining": amount,
|
||||
"cost": 25.0 * amount,
|
||||
"fee": None,
|
||||
}
|
||||
|
||||
result = exchange.check_dry_limit_order_filled(order, immediate=immediate)
|
||||
|
||||
assert result["status"] == expected_status
|
||||
if crossed:
|
||||
assert result["filled"] == amount
|
||||
assert result["remaining"] == 0.0
|
||||
assert result["fee"]["rate"] == expected_fee_rate
|
||||
fee_mock.assert_called_once_with("LTC/USDT", taker_or_maker=taker_or_maker)
|
||||
else:
|
||||
assert result["filled"] == 0.0
|
||||
assert result["remaining"] == amount
|
||||
assert result["fee"] is None
|
||||
assert fee_mock.call_count == expected_calls
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"immediate,crossed,expected_status,expected_fee_type",
|
||||
[
|
||||
(True, True, "closed", "taker"),
|
||||
(False, True, "closed", "maker"),
|
||||
(True, False, "open", None),
|
||||
],
|
||||
)
|
||||
def test_check_dry_limit_order_filled_stoploss(
|
||||
default_conf, mocker, immediate, crossed, expected_status, expected_fee_type, order_book_l2_usd
|
||||
):
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
mocker.patch.multiple(
|
||||
EXMS,
|
||||
exchange_has=MagicMock(return_value=True),
|
||||
_dry_is_price_crossed=MagicMock(return_value=crossed),
|
||||
fetch_l2_order_book=order_book_l2_usd,
|
||||
)
|
||||
average_mock = mocker.patch(f"{EXMS}.get_dry_market_fill_price", return_value=24.25)
|
||||
fee_mock = mocker.patch(
|
||||
f"{EXMS}.add_dry_order_fee",
|
||||
autospec=True,
|
||||
side_effect=lambda self, pair, dry_order, taker_or_maker: dry_order,
|
||||
)
|
||||
|
||||
amount = 1.75
|
||||
order = {
|
||||
"symbol": "LTC/USDT",
|
||||
"status": "open",
|
||||
"type": "limit",
|
||||
"side": "sell",
|
||||
"amount": amount,
|
||||
"filled": 0.0,
|
||||
"remaining": amount,
|
||||
"price": 25.0,
|
||||
"average": 0.0,
|
||||
"cost": 0.0,
|
||||
"fee": None,
|
||||
"ft_order_type": "stoploss",
|
||||
"stopLossPrice": 24.5,
|
||||
}
|
||||
|
||||
result = exchange.check_dry_limit_order_filled(order, immediate=immediate)
|
||||
|
||||
assert result["status"] == expected_status
|
||||
assert order_book_l2_usd.call_count == 1
|
||||
if crossed:
|
||||
assert result["filled"] == amount
|
||||
assert result["remaining"] == 0
|
||||
assert result["average"] == 24.25
|
||||
assert result["cost"] == pytest.approx(amount * 24.25)
|
||||
assert average_mock.call_count == 1
|
||||
assert fee_mock.call_count == 1
|
||||
assert fee_mock.call_args[0][1] == "LTC/USDT"
|
||||
assert fee_mock.call_args[0][3] == expected_fee_type
|
||||
else:
|
||||
assert result["filled"] == 0.0
|
||||
assert result["remaining"] == amount
|
||||
assert result["average"] == 0.0
|
||||
|
||||
assert average_mock.call_count == 0
|
||||
assert fee_mock.call_count == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"side,price,filled,converted",
|
||||
[
|
||||
@@ -2176,10 +2361,7 @@ def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name, candle_
|
||||
since = date_minus_candles("5m", candle_limit)
|
||||
ret = exchange.get_historic_ohlcv(pair, "5m", dt_ts(since), candle_type=candle_type)
|
||||
|
||||
if exchange_name == "okx" and candle_type == "mark":
|
||||
expected = 4
|
||||
else:
|
||||
expected = 2
|
||||
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) == expected
|
||||
@@ -2207,6 +2389,7 @@ async def test__async_get_historic_ohlcv(default_conf, mocker, caplog, exchange_
|
||||
]
|
||||
]
|
||||
exchange = get_patched_exchange(mocker, default_conf, exchange=exchange_name)
|
||||
mocker.patch.object(exchange, "verify_candle_type_support")
|
||||
# Monkey-patch async function
|
||||
exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv)
|
||||
|
||||
@@ -2257,6 +2440,7 @@ def test_refresh_latest_ohlcv(mocker, default_conf_usdt, caplog, candle_type) ->
|
||||
|
||||
caplog.set_level(logging.DEBUG)
|
||||
exchange = get_patched_exchange(mocker, default_conf_usdt)
|
||||
mocker.patch.object(exchange, "verify_candle_type_support")
|
||||
exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv)
|
||||
|
||||
pairs = [("IOTA/USDT", "5m", candle_type), ("XRP/USDT", "5m", candle_type)]
|
||||
@@ -2507,6 +2691,7 @@ def test_refresh_latest_ohlcv_cache(mocker, default_conf, candle_type, time_mach
|
||||
time_machine.move_to(start + timedelta(hours=99, minutes=30))
|
||||
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
mocker.patch.object(exchange, "verify_candle_type_support")
|
||||
exchange._set_startup_candle_count(default_conf)
|
||||
|
||||
mocker.patch(f"{EXMS}.ohlcv_candle_limit", return_value=100)
|
||||
@@ -2609,8 +2794,10 @@ def test_refresh_ohlcv_with_cache(mocker, default_conf, time_machine) -> None:
|
||||
("LTC/BTC", "1h", CandleType.SPOT),
|
||||
]
|
||||
|
||||
ohlcv_data = {p: ohlcv for p in pairs}
|
||||
ohlcv_mock = mocker.patch(f"{EXMS}.refresh_latest_ohlcv", return_value=ohlcv_data)
|
||||
def ohlcv_side_effect(requested_pairs, *args, **kwargs):
|
||||
return {p: ohlcv for p in requested_pairs}
|
||||
|
||||
ohlcv_mock = mocker.patch(f"{EXMS}.refresh_latest_ohlcv", side_effect=ohlcv_side_effect)
|
||||
mocker.patch(f"{EXMS}.ohlcv_candle_limit", return_value=100)
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
|
||||
@@ -2628,6 +2815,14 @@ def test_refresh_ohlcv_with_cache(mocker, default_conf, time_machine) -> None:
|
||||
ohlcv_mock.reset_mock()
|
||||
res = exchange.refresh_ohlcv_with_cache(pairs, start.timestamp())
|
||||
assert ohlcv_mock.call_count == 0
|
||||
assert len(res) == 5
|
||||
|
||||
# # re-run with one additional pair
|
||||
res = exchange.refresh_ohlcv_with_cache(
|
||||
pairs + [("NEW/PAIR", "1d", CandleType.SPOT)], start.timestamp()
|
||||
)
|
||||
assert ohlcv_mock.call_count == 1
|
||||
assert len(res) == 6
|
||||
|
||||
# Expire 5m cache
|
||||
time_machine.move_to(start + timedelta(minutes=6), tick=False)
|
||||
@@ -2636,6 +2831,7 @@ def test_refresh_ohlcv_with_cache(mocker, default_conf, time_machine) -> None:
|
||||
res = exchange.refresh_ohlcv_with_cache(pairs, start.timestamp())
|
||||
assert ohlcv_mock.call_count == 1
|
||||
assert len(ohlcv_mock.call_args_list[0][0][0]) == 1
|
||||
assert len(res) == 5
|
||||
|
||||
# Expire 5m and 1h cache
|
||||
time_machine.move_to(start + timedelta(hours=2), tick=False)
|
||||
@@ -2644,6 +2840,7 @@ def test_refresh_ohlcv_with_cache(mocker, default_conf, time_machine) -> None:
|
||||
res = exchange.refresh_ohlcv_with_cache(pairs, start.timestamp())
|
||||
assert ohlcv_mock.call_count == 1
|
||||
assert len(ohlcv_mock.call_args_list[0][0][0]) == 2
|
||||
assert len(res) == 5
|
||||
|
||||
# Expire all caches
|
||||
time_machine.move_to(start + timedelta(days=1, hours=2), tick=False)
|
||||
@@ -2653,6 +2850,30 @@ def test_refresh_ohlcv_with_cache(mocker, default_conf, time_machine) -> None:
|
||||
assert ohlcv_mock.call_count == 1
|
||||
assert len(ohlcv_mock.call_args_list[0][0][0]) == 5
|
||||
assert ohlcv_mock.call_args_list[0][0][0] == pairs
|
||||
assert len(res) == 5
|
||||
|
||||
|
||||
def test_refresh_latest_ohlcv_funding_rate(mocker, default_conf_usdt, caplog) -> None:
|
||||
ohlcv = generate_test_data_raw("1h", 24, "2025-01-02 12:00:00+00:00")
|
||||
funding_data = [{"timestamp": x[0], "fundingRate": x[1]} for x in ohlcv]
|
||||
|
||||
caplog.set_level(logging.DEBUG)
|
||||
exchange = get_patched_exchange(mocker, default_conf_usdt)
|
||||
exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv)
|
||||
exchange._api_async.fetch_funding_rate_history = get_mock_coro(funding_data)
|
||||
|
||||
pairs = [
|
||||
("IOTA/USDT:USDT", "8h", CandleType.FUNDING_RATE),
|
||||
("XRP/USDT:USDT", "1h", CandleType.FUNDING_RATE),
|
||||
]
|
||||
# empty dicts
|
||||
assert not exchange._klines
|
||||
res = exchange.refresh_latest_ohlcv(pairs, cache=False)
|
||||
|
||||
assert len(res) == len(pairs)
|
||||
assert log_has_re(r"Wrong funding rate timeframe 8h for pair IOTA/USDT:USDT", caplog)
|
||||
assert not log_has_re(r"Wrong funding rate timeframe 8h for pair XRP/USDT:USDT", caplog)
|
||||
assert exchange._api_async.fetch_ohlcv.call_count == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||
@@ -3719,37 +3940,29 @@ def test_cancel_stoploss_order(default_conf, mocker, exchange_name):
|
||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||
def test_cancel_stoploss_order_with_result(default_conf, mocker, exchange_name):
|
||||
default_conf["dry_run"] = False
|
||||
mock_prefix = "freqtrade.exchange.gate.Gate"
|
||||
if exchange_name == "okx":
|
||||
mock_prefix = "freqtrade.exchange.okx.Okx"
|
||||
mocker.patch(f"{EXMS}.fetch_stoploss_order", return_value={"for": 123})
|
||||
mocker.patch(f"{mock_prefix}.fetch_stoploss_order", return_value={"for": 123})
|
||||
exchange = get_patched_exchange(mocker, default_conf, exchange=exchange_name)
|
||||
mocker.patch.object(exchange, "fetch_stoploss_order", return_value={"for": 123})
|
||||
|
||||
res = {"fee": {}, "status": "canceled", "amount": 1234}
|
||||
mocker.patch(f"{EXMS}.cancel_stoploss_order", return_value=res)
|
||||
mocker.patch(f"{mock_prefix}.cancel_stoploss_order", return_value=res)
|
||||
mocker.patch.object(exchange, "cancel_stoploss_order", return_value=res)
|
||||
co = exchange.cancel_stoploss_order_with_result(order_id="_", pair="TKN/BTC", amount=555)
|
||||
assert co == res
|
||||
|
||||
mocker.patch(f"{EXMS}.cancel_stoploss_order", return_value="canceled")
|
||||
mocker.patch(f"{mock_prefix}.cancel_stoploss_order", return_value="canceled")
|
||||
mocker.patch.object(exchange, "cancel_stoploss_order", return_value="canceled")
|
||||
# Fall back to fetch_stoploss_order
|
||||
co = exchange.cancel_stoploss_order_with_result(order_id="_", pair="TKN/BTC", amount=555)
|
||||
assert co == {"for": 123}
|
||||
|
||||
exc = InvalidOrderException("")
|
||||
mocker.patch(f"{EXMS}.fetch_stoploss_order", side_effect=exc)
|
||||
mocker.patch(f"{mock_prefix}.fetch_stoploss_order", side_effect=exc)
|
||||
mocker.patch.object(exchange, "fetch_stoploss_order", side_effect=exc)
|
||||
co = exchange.cancel_stoploss_order_with_result(order_id="_", pair="TKN/BTC", amount=555)
|
||||
assert co["amount"] == 555
|
||||
assert co == {"id": "_", "fee": {}, "status": "canceled", "amount": 555, "info": {}}
|
||||
|
||||
with pytest.raises(InvalidOrderException):
|
||||
exc = InvalidOrderException("Did not find order")
|
||||
mocker.patch(f"{EXMS}.cancel_stoploss_order", side_effect=exc)
|
||||
mocker.patch(f"{mock_prefix}.cancel_stoploss_order", side_effect=exc)
|
||||
exchange = get_patched_exchange(mocker, default_conf, exchange=exchange_name)
|
||||
mocker.patch.object(exchange, "cancel_stoploss_order", side_effect=exc)
|
||||
exchange.cancel_stoploss_order_with_result(order_id="_", pair="TKN/BTC", amount=123)
|
||||
|
||||
|
||||
@@ -3934,7 +4147,7 @@ def test_fetch_order_or_stoploss_order(default_conf, mocker):
|
||||
fetch_order_mock = MagicMock()
|
||||
fetch_stoploss_order_mock = MagicMock()
|
||||
mocker.patch.multiple(
|
||||
EXMS,
|
||||
exchange,
|
||||
fetch_order=fetch_order_mock,
|
||||
fetch_stoploss_order=fetch_stoploss_order_mock,
|
||||
)
|
||||
@@ -4900,53 +5113,66 @@ def test_set_margin_mode(mocker, default_conf, margin_mode):
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"exchange_name, trading_mode, margin_mode, exception_thrown",
|
||||
"exchange_name, trading_mode, margin_mode, allow_none_margin_mode, exception_thrown",
|
||||
[
|
||||
("binance", TradingMode.SPOT, None, False),
|
||||
("binance", TradingMode.MARGIN, MarginMode.ISOLATED, True),
|
||||
("kraken", TradingMode.SPOT, None, False),
|
||||
("kraken", TradingMode.MARGIN, MarginMode.ISOLATED, True),
|
||||
("kraken", TradingMode.FUTURES, MarginMode.ISOLATED, True),
|
||||
("bitmart", TradingMode.SPOT, None, False),
|
||||
("bitmart", TradingMode.MARGIN, MarginMode.CROSS, True),
|
||||
("bitmart", TradingMode.MARGIN, MarginMode.ISOLATED, True),
|
||||
("bitmart", TradingMode.FUTURES, MarginMode.CROSS, True),
|
||||
("bitmart", TradingMode.FUTURES, MarginMode.ISOLATED, True),
|
||||
("gate", TradingMode.MARGIN, MarginMode.ISOLATED, True),
|
||||
("okx", TradingMode.SPOT, None, False),
|
||||
("okx", TradingMode.MARGIN, MarginMode.CROSS, True),
|
||||
("okx", TradingMode.MARGIN, MarginMode.ISOLATED, True),
|
||||
("okx", TradingMode.FUTURES, MarginMode.CROSS, True),
|
||||
("binance", TradingMode.FUTURES, MarginMode.ISOLATED, False),
|
||||
("gate", TradingMode.FUTURES, MarginMode.ISOLATED, False),
|
||||
("okx", TradingMode.FUTURES, MarginMode.ISOLATED, False),
|
||||
("binance", TradingMode.SPOT, None, False, False),
|
||||
("binance", TradingMode.MARGIN, MarginMode.ISOLATED, False, True),
|
||||
("kraken", TradingMode.SPOT, None, False, False),
|
||||
("kraken", TradingMode.MARGIN, MarginMode.ISOLATED, False, True),
|
||||
("kraken", TradingMode.FUTURES, MarginMode.ISOLATED, False, True),
|
||||
("bitmart", TradingMode.SPOT, None, False, False),
|
||||
("bitmart", TradingMode.MARGIN, MarginMode.CROSS, False, True),
|
||||
("bitmart", TradingMode.MARGIN, MarginMode.ISOLATED, False, True),
|
||||
("bitmart", TradingMode.FUTURES, MarginMode.CROSS, False, True),
|
||||
("bitmart", TradingMode.FUTURES, MarginMode.ISOLATED, False, True),
|
||||
("gate", TradingMode.MARGIN, MarginMode.ISOLATED, False, True),
|
||||
("okx", TradingMode.SPOT, None, False, False),
|
||||
("okx", TradingMode.MARGIN, MarginMode.CROSS, False, True),
|
||||
("okx", TradingMode.MARGIN, MarginMode.ISOLATED, False, True),
|
||||
("okx", TradingMode.FUTURES, MarginMode.CROSS, False, True),
|
||||
("binance", TradingMode.FUTURES, MarginMode.ISOLATED, False, False),
|
||||
("gate", TradingMode.FUTURES, MarginMode.ISOLATED, False, False),
|
||||
("okx", TradingMode.FUTURES, MarginMode.ISOLATED, False, False),
|
||||
# * Remove once implemented
|
||||
("binance", TradingMode.MARGIN, MarginMode.CROSS, True),
|
||||
("binance", TradingMode.FUTURES, MarginMode.CROSS, False),
|
||||
("kraken", TradingMode.MARGIN, MarginMode.CROSS, True),
|
||||
("kraken", TradingMode.FUTURES, MarginMode.CROSS, True),
|
||||
("gate", TradingMode.MARGIN, MarginMode.CROSS, True),
|
||||
("gate", TradingMode.FUTURES, MarginMode.CROSS, True),
|
||||
("binance", TradingMode.MARGIN, MarginMode.CROSS, False, True),
|
||||
("binance", TradingMode.FUTURES, MarginMode.CROSS, False, False),
|
||||
("binance", TradingMode.FUTURES, None, False, True),
|
||||
# Validate without margin mode
|
||||
("binance", TradingMode.FUTURES, None, True, False),
|
||||
("kraken", TradingMode.MARGIN, MarginMode.CROSS, False, True),
|
||||
("kraken", TradingMode.FUTURES, MarginMode.CROSS, False, True),
|
||||
("gate", TradingMode.MARGIN, MarginMode.CROSS, False, True),
|
||||
("gate", TradingMode.FUTURES, MarginMode.CROSS, False, True),
|
||||
# * Uncomment once implemented
|
||||
# ("binance", TradingMode.MARGIN, MarginMode.CROSS, False),
|
||||
# ("binance", TradingMode.FUTURES, MarginMode.CROSS, False),
|
||||
# ("kraken", TradingMode.MARGIN, MarginMode.CROSS, False),
|
||||
# ("kraken", TradingMode.FUTURES, MarginMode.CROSS, False),
|
||||
# ("gate", TradingMode.MARGIN, MarginMode.CROSS, False),
|
||||
# ("gate", TradingMode.FUTURES, MarginMode.CROSS, False),
|
||||
# ("binance", TradingMode.MARGIN, MarginMode.CROSS, False, False),
|
||||
# ("binance", TradingMode.FUTURES, MarginMode.CROSS, False, False),
|
||||
# ("kraken", TradingMode.MARGIN, MarginMode.CROSS, False, False),
|
||||
# ("kraken", TradingMode.FUTURES, MarginMode.CROSS, False, False),
|
||||
# ("gate", TradingMode.MARGIN, MarginMode.CROSS, False, False),
|
||||
# ("gate", TradingMode.FUTURES, MarginMode.CROSS, False, False),
|
||||
],
|
||||
)
|
||||
def test_validate_trading_mode_and_margin_mode(
|
||||
default_conf, mocker, exchange_name, trading_mode, margin_mode, exception_thrown
|
||||
default_conf,
|
||||
mocker,
|
||||
exchange_name,
|
||||
trading_mode,
|
||||
margin_mode,
|
||||
allow_none_margin_mode,
|
||||
exception_thrown,
|
||||
):
|
||||
exchange = get_patched_exchange(
|
||||
mocker, default_conf, exchange=exchange_name, mock_supported_modes=False
|
||||
)
|
||||
if exception_thrown:
|
||||
with pytest.raises(OperationalException):
|
||||
exchange.validate_trading_mode_and_margin_mode(trading_mode, margin_mode)
|
||||
exchange.validate_trading_mode_and_margin_mode(
|
||||
trading_mode, margin_mode, allow_none_margin_mode
|
||||
)
|
||||
else:
|
||||
exchange.validate_trading_mode_and_margin_mode(trading_mode, margin_mode)
|
||||
exchange.validate_trading_mode_and_margin_mode(
|
||||
trading_mode, margin_mode, allow_none_margin_mode
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -5109,6 +5335,7 @@ def test_combine_funding_and_mark(
|
||||
{"date": trade_date, "open": mark_price},
|
||||
]
|
||||
)
|
||||
# Test fallback to futures funding rate for missing funding rates
|
||||
df = exchange.combine_funding_and_mark(funding_rates, mark_rates, futures_funding_rate)
|
||||
|
||||
if futures_funding_rate is not None:
|
||||
@@ -5136,6 +5363,34 @@ def test_combine_funding_and_mark(
|
||||
|
||||
assert len(df) == 0
|
||||
|
||||
# Test fallback to futures funding rate for middle missing funding rate
|
||||
funding_rates = DataFrame(
|
||||
[
|
||||
{"date": prior2_date, "open": funding_rate},
|
||||
# missing 1 hour
|
||||
{"date": trade_date, "open": funding_rate},
|
||||
],
|
||||
)
|
||||
mark_rates = DataFrame(
|
||||
[
|
||||
{"date": prior2_date, "open": mark_price},
|
||||
{"date": prior_date, "open": mark_price},
|
||||
{"date": trade_date, "open": mark_price},
|
||||
]
|
||||
)
|
||||
df = exchange.combine_funding_and_mark(funding_rates, mark_rates, futures_funding_rate)
|
||||
|
||||
if futures_funding_rate is not None:
|
||||
assert len(df) == 2
|
||||
assert df.iloc[0]["open_fund"] == funding_rate
|
||||
# assert df.iloc[1]["open_fund"] == futures_funding_rate
|
||||
assert df.iloc[-1]["open_fund"] == funding_rate
|
||||
# Mid-candle is dropped ...
|
||||
assert df["date"].to_list() == [prior2_date, trade_date]
|
||||
else:
|
||||
assert len(df) == 2
|
||||
assert df["date"].to_list() == [prior2_date, trade_date]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"exchange,rate_start,rate_end,d1,d2,amount,expected_fees",
|
||||
@@ -5225,8 +5480,13 @@ def test__fetch_and_calculate_funding_fees(
|
||||
api_mock = MagicMock()
|
||||
api_mock.fetch_funding_rate_history = get_mock_coro(return_value=funding_rate_history)
|
||||
api_mock.fetch_ohlcv = get_mock_coro(return_value=mark_ohlcv)
|
||||
type(api_mock).has = PropertyMock(return_value={"fetchOHLCV": True})
|
||||
type(api_mock).has = PropertyMock(return_value={"fetchFundingRateHistory": True})
|
||||
type(api_mock).has = PropertyMock(
|
||||
return_value={
|
||||
"fetchFundingRateHistory": True,
|
||||
"fetchMarkOHLCV": True,
|
||||
"fetchOHLCV": True,
|
||||
}
|
||||
)
|
||||
|
||||
ex = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange)
|
||||
mocker.patch(f"{EXMS}.timeframes", PropertyMock(return_value=["1h", "4h", "8h"]))
|
||||
@@ -5270,8 +5530,13 @@ def test__fetch_and_calculate_funding_fees_datetime_called(
|
||||
api_mock.fetch_funding_rate_history = get_mock_coro(
|
||||
return_value=funding_rate_history_octohourly
|
||||
)
|
||||
type(api_mock).has = PropertyMock(return_value={"fetchOHLCV": True})
|
||||
type(api_mock).has = PropertyMock(return_value={"fetchFundingRateHistory": True})
|
||||
type(api_mock).has = PropertyMock(
|
||||
return_value={
|
||||
"fetchFundingRateHistory": True,
|
||||
"fetchMarkOHLCV": True,
|
||||
"fetchOHLCV": True,
|
||||
}
|
||||
)
|
||||
mocker.patch(f"{EXMS}.timeframes", PropertyMock(return_value=["4h", "8h"]))
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange)
|
||||
d1 = datetime.strptime("2021-08-31 23:00:01 +0000", "%Y-%m-%d %H:%M:%S %z")
|
||||
@@ -5930,6 +6195,10 @@ def test_get_max_leverage_futures(default_conf, mocker, leverage_tiers):
|
||||
assert exchange.get_max_leverage("BTC/USDT:USDT", 300000000) == 2.0
|
||||
assert exchange.get_max_leverage("BTC/USDT:USDT", 600000000) == 1.0 # Last tier
|
||||
|
||||
# Test ADA - last maxNotional is None
|
||||
assert exchange.get_max_leverage("ADA/USDT:USDT", 2500000) == 2.0 # Second last tier
|
||||
assert exchange.get_max_leverage("ADA/USDT:USDT", 6000000) == 1.0 # Last tier, open maxNotional
|
||||
|
||||
assert exchange.get_max_leverage("SPONGE/USDT:USDT", 200) == 1.0 # Pair not in leverage_tiers
|
||||
assert exchange.get_max_leverage("BTC/USDT:USDT", 0.0) == 125.0 # No stake amount
|
||||
with pytest.raises(
|
||||
@@ -5942,29 +6211,32 @@ def test_get_max_leverage_futures(default_conf, mocker, leverage_tiers):
|
||||
assert exchange.get_max_leverage("TIA/USDT:USDT", 130.008) == 40
|
||||
|
||||
|
||||
@pytest.mark.parametrize("exchange_name", ["binance", "kraken", "gate", "okx", "bybit"])
|
||||
def test__get_params(mocker, default_conf, exchange_name):
|
||||
@pytest.mark.parametrize(
|
||||
"exchange_name, add_params_spot, add_params_futures",
|
||||
[
|
||||
("binance", {}, {}),
|
||||
("kraken", {}, {"leverage": 3.0}),
|
||||
("gate", {}, {}),
|
||||
("okx", {}, {"tdMode": "isolated", "posSide": "net"}),
|
||||
("bybit", {}, {"position_idx": 0}),
|
||||
("bitget", {}, {"marginMode": "isolated"}),
|
||||
],
|
||||
)
|
||||
def test__get_params(mocker, default_conf, exchange_name, add_params_spot, add_params_futures):
|
||||
api_mock = MagicMock()
|
||||
mocker.patch(f"{EXMS}.exchange_has", return_value=True)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name)
|
||||
exchange._params = {"test": True}
|
||||
|
||||
params1 = {"test": True}
|
||||
params2 = {
|
||||
params1.update(add_params_spot)
|
||||
|
||||
params_fut = {
|
||||
"test": True,
|
||||
"timeInForce": "IOC",
|
||||
"reduceOnly": True,
|
||||
}
|
||||
|
||||
if exchange_name == "kraken":
|
||||
params2["leverage"] = 3.0
|
||||
|
||||
if exchange_name == "okx":
|
||||
params2["tdMode"] = "isolated"
|
||||
params2["posSide"] = "net"
|
||||
|
||||
if exchange_name == "bybit":
|
||||
params2["position_idx"] = 0
|
||||
params_fut.update(add_params_futures)
|
||||
|
||||
assert (
|
||||
exchange._get_params(
|
||||
@@ -6012,7 +6284,7 @@ def test__get_params(mocker, default_conf, exchange_name):
|
||||
time_in_force="IOC",
|
||||
leverage=3.0,
|
||||
)
|
||||
== params2
|
||||
== params_fut
|
||||
)
|
||||
|
||||
|
||||
@@ -6355,3 +6627,51 @@ def test_fetch_funding_rate(default_conf, mocker, exchange_name):
|
||||
|
||||
with pytest.raises(DependencyException, match=r"Pair XRP/ETH not available"):
|
||||
exchange.fetch_funding_rate(pair="XRP/ETH")
|
||||
|
||||
|
||||
def test_verify_candle_type_support(default_conf, mocker):
|
||||
api_mock = MagicMock()
|
||||
type(api_mock).has = PropertyMock(
|
||||
return_value={
|
||||
"fetchFundingRateHistory": True,
|
||||
"fetchIndexOHLCV": True,
|
||||
"fetchMarkOHLCV": True,
|
||||
"fetchPremiumIndexOHLCV": False,
|
||||
}
|
||||
)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
|
||||
# Should pass
|
||||
exchange.verify_candle_type_support("futures")
|
||||
exchange.verify_candle_type_support(CandleType.FUTURES)
|
||||
exchange.verify_candle_type_support(CandleType.FUNDING_RATE)
|
||||
exchange.verify_candle_type_support(CandleType.SPOT)
|
||||
exchange.verify_candle_type_support(CandleType.MARK)
|
||||
|
||||
# Should fail:
|
||||
|
||||
with pytest.raises(
|
||||
OperationalException,
|
||||
match=r"Exchange .* does not support fetching premiumindex candles\.",
|
||||
):
|
||||
exchange.verify_candle_type_support(CandleType.PREMIUMINDEX)
|
||||
|
||||
type(api_mock).has = PropertyMock(
|
||||
return_value={
|
||||
"fetchFundingRateHistory": False,
|
||||
"fetchIndexOHLCV": False,
|
||||
"fetchMarkOHLCV": False,
|
||||
"fetchPremiumIndexOHLCV": True,
|
||||
}
|
||||
)
|
||||
for candle_type in [
|
||||
CandleType.FUNDING_RATE,
|
||||
CandleType.INDEX,
|
||||
CandleType.MARK,
|
||||
]:
|
||||
with pytest.raises(
|
||||
OperationalException,
|
||||
match=rf"Exchange .* does not support fetching {candle_type.value} candles\.",
|
||||
):
|
||||
exchange.verify_candle_type_support(candle_type)
|
||||
exchange.verify_candle_type_support(CandleType.PREMIUMINDEX)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# pragma pylint: disable=missing-docstring, protected-access, invalid-name
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from math import isnan, nan
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
from ccxt import (
|
||||
@@ -28,6 +29,7 @@ from freqtrade.exchange import (
|
||||
timeframe_to_seconds,
|
||||
)
|
||||
from freqtrade.exchange.check_exchange import check_exchange
|
||||
from freqtrade.exchange.exchange_utils import _exchange_has_helper
|
||||
from tests.conftest import log_has_re
|
||||
|
||||
|
||||
@@ -385,3 +387,42 @@ def test_amount_to_contract_precision_standalone(
|
||||
):
|
||||
res = amount_to_contract_precision(amount, precision, precision_mode, contract_size)
|
||||
assert pytest.approx(res) == expected
|
||||
|
||||
|
||||
def test_exchange__exchange_has_helper():
|
||||
e_mod = MagicMock()
|
||||
e_mod.has = {
|
||||
"fetchTicker": True,
|
||||
"fetchOHLCV": False,
|
||||
"fetchTrades": True,
|
||||
"fetchMyTrades": False,
|
||||
"fetchOrder": True,
|
||||
}
|
||||
required = {
|
||||
"fetchOHLCV": [],
|
||||
"fetchTicker": [],
|
||||
"fetchMyTrades": ["fetchTrades"],
|
||||
"fetchOrder": ["fetchOpenOrder", "fetchClosedOrder"],
|
||||
}
|
||||
missing = _exchange_has_helper(e_mod, required)
|
||||
assert set(missing) == {"fetchOHLCV"}
|
||||
|
||||
e_mod.has = {
|
||||
"fetchTicker": True,
|
||||
"fetchOHLCV": False,
|
||||
"fetchTrades": False,
|
||||
"fetchMyTrades": False,
|
||||
"fetchOrder": True,
|
||||
}
|
||||
missing = _exchange_has_helper(e_mod, required)
|
||||
assert set(missing) == {"fetchOHLCV", "fetchMyTrades"}
|
||||
|
||||
e_mod.has = {
|
||||
"fetchTicker": True,
|
||||
"fetchOHLCV": False,
|
||||
"fetchTrades": False,
|
||||
"fetchMyTrades": False,
|
||||
"fetchOrder": False,
|
||||
}
|
||||
missing = _exchange_has_helper(e_mod, required)
|
||||
assert set(missing) == {"fetchOHLCV", "fetchMyTrades", "fetchOrder"}
|
||||
|
||||
@@ -16,9 +16,9 @@ def test_fetch_stoploss_order_gate(default_conf, mocker):
|
||||
|
||||
exchange.fetch_stoploss_order("1234", "ETH/BTC")
|
||||
assert fetch_order_mock.call_count == 1
|
||||
assert fetch_order_mock.call_args_list[0][1]["order_id"] == "1234"
|
||||
assert fetch_order_mock.call_args_list[0][1]["pair"] == "ETH/BTC"
|
||||
assert fetch_order_mock.call_args_list[0][1]["params"] == {"stop": True}
|
||||
assert fetch_order_mock.call_args_list[0][0][0] == "1234"
|
||||
assert fetch_order_mock.call_args_list[0][0][1] == "ETH/BTC"
|
||||
assert fetch_order_mock.call_args_list[0][0][2] == {"stop": True}
|
||||
|
||||
default_conf["trading_mode"] = "futures"
|
||||
default_conf["margin_mode"] = "isolated"
|
||||
@@ -36,21 +36,19 @@ def test_fetch_stoploss_order_gate(default_conf, mocker):
|
||||
|
||||
exchange.fetch_stoploss_order("1234", "ETH/BTC")
|
||||
assert exchange.fetch_order.call_count == 2
|
||||
assert exchange.fetch_order.call_args_list[0][1]["order_id"] == "1234"
|
||||
assert exchange.fetch_order.call_args_list[0][0][0] == "1234"
|
||||
assert exchange.fetch_order.call_args_list[1][1]["order_id"] == "222555"
|
||||
|
||||
|
||||
def test_cancel_stoploss_order_gate(default_conf, mocker):
|
||||
exchange = get_patched_exchange(mocker, default_conf, exchange="gate")
|
||||
|
||||
cancel_order_mock = MagicMock()
|
||||
exchange.cancel_order = cancel_order_mock
|
||||
cancel_order_mock = mocker.patch.object(exchange, "cancel_order", autospec=True)
|
||||
|
||||
exchange.cancel_stoploss_order("1234", "ETH/BTC")
|
||||
assert cancel_order_mock.call_count == 1
|
||||
assert cancel_order_mock.call_args_list[0][1]["order_id"] == "1234"
|
||||
assert cancel_order_mock.call_args_list[0][1]["pair"] == "ETH/BTC"
|
||||
assert cancel_order_mock.call_args_list[0][1]["params"] == {"stop": True}
|
||||
assert cancel_order_mock.call_args_list[0][0][0] == "1234"
|
||||
assert cancel_order_mock.call_args_list[0][0][1] == "ETH/BTC"
|
||||
assert cancel_order_mock.call_args_list[0][0][2] == {"stop": True}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
@@ -123,7 +123,8 @@ def test_create_stoploss_order_dry_run_htx(default_conf, mocker):
|
||||
assert "type" in order
|
||||
|
||||
assert order["type"] == order_type
|
||||
assert order["price"] == 220
|
||||
assert order["price"] == 217.8
|
||||
assert order["stopPrice"] == 220
|
||||
assert order["amount"] == 1
|
||||
|
||||
|
||||
|
||||
@@ -1,20 +1,125 @@
|
||||
from copy import deepcopy
|
||||
from datetime import UTC, datetime
|
||||
from unittest.mock import MagicMock, PropertyMock
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.conftest import EXMS, get_mock_coro, get_patched_exchange
|
||||
from freqtrade.exceptions import ConfigurationError
|
||||
from tests.conftest import EXMS, get_mock_coro, get_patched_exchange, log_has_re
|
||||
|
||||
|
||||
def test_hyperliquid_dry_run_liquidation_price(default_conf, mocker):
|
||||
@pytest.fixture
|
||||
def markets_hip3():
|
||||
markets = {
|
||||
"BTC/USDC:USDC": {
|
||||
"quote": "USDC",
|
||||
"base": "BTC",
|
||||
"type": "swap",
|
||||
"swap": True,
|
||||
"linear": True,
|
||||
"limits": {"leverage": {"max": 50}},
|
||||
"info": {},
|
||||
},
|
||||
"ETH/USDC:USDC": {
|
||||
"quote": "USDC",
|
||||
"base": "ETH",
|
||||
"type": "swap",
|
||||
"swap": True,
|
||||
"linear": True,
|
||||
"limits": {"leverage": {"max": 50}},
|
||||
"info": {},
|
||||
},
|
||||
"SOL/USDC:USDC": {
|
||||
"quote": "USDC",
|
||||
"base": "SOL",
|
||||
"type": "swap",
|
||||
"swap": True,
|
||||
"linear": True,
|
||||
"limits": {"leverage": {"max": 20}},
|
||||
"info": {},
|
||||
},
|
||||
"DOGE/USDC:USDC": {
|
||||
"quote": "USDC",
|
||||
"base": "DOGE",
|
||||
"type": "swap",
|
||||
"swap": True,
|
||||
"linear": True,
|
||||
"limits": {"leverage": {"max": 20}},
|
||||
"info": {},
|
||||
},
|
||||
"XYZ-AAPL/USDC:USDC": {
|
||||
"quote": "USDC",
|
||||
"base": "XYZ-AAPL",
|
||||
"type": "swap",
|
||||
"swap": True,
|
||||
"linear": True,
|
||||
"limits": {"leverage": {"max": 10}},
|
||||
"info": {"hip3": True, "dex": "xyz"},
|
||||
},
|
||||
"XYZ-TSLA/USDC:USDC": {
|
||||
"quote": "USDC",
|
||||
"base": "XYZ-TSLA",
|
||||
"type": "swap",
|
||||
"swap": True,
|
||||
"linear": True,
|
||||
"limits": {"leverage": {"max": 10}},
|
||||
"info": {"hip3": True, "dex": "xyz"},
|
||||
},
|
||||
"XYZ-GOOGL/USDC:USDC": {
|
||||
"quote": "USDC",
|
||||
"base": "XYZ-GOOGL",
|
||||
"type": "swap",
|
||||
"swap": True,
|
||||
"linear": True,
|
||||
"limits": {"leverage": {"max": 10}},
|
||||
"info": {"hip3": True, "dex": "xyz"},
|
||||
},
|
||||
"XYZ-NVDA/USDC:USDC": {
|
||||
"quote": "USDC",
|
||||
"base": "XYZ-NVDA",
|
||||
"type": "swap",
|
||||
"swap": True,
|
||||
"linear": True,
|
||||
"limits": {"leverage": {"max": 10}},
|
||||
"info": {"hip3": True, "dex": "xyz"},
|
||||
},
|
||||
"VNTL-SPACEX/USDH:USDH": {
|
||||
"quote": "USDH",
|
||||
"base": "VNTL-SPACEX",
|
||||
"type": "swap",
|
||||
"swap": True,
|
||||
"linear": True,
|
||||
"limits": {"leverage": {"max": 3}},
|
||||
"info": {"hip3": True, "dex": "vntl"},
|
||||
},
|
||||
"VNTL-ANTHROPIC/USDH:USDH": {
|
||||
"quote": "USDH",
|
||||
"base": "VNTL-ANTHROPIC",
|
||||
"type": "swap",
|
||||
"swap": True,
|
||||
"linear": True,
|
||||
"limits": {"leverage": {"max": 3}},
|
||||
"info": {"hip3": True, "dex": "vntl"},
|
||||
},
|
||||
"FLX-TOKEN/USDC:USDC": {
|
||||
"quote": "USDC",
|
||||
"base": "FLX-TOKEN",
|
||||
"type": "swap",
|
||||
"swap": True,
|
||||
"linear": True,
|
||||
"limits": {"leverage": {"max": 3}},
|
||||
"info": {"hip3": True, "dex": "flx"},
|
||||
},
|
||||
}
|
||||
|
||||
return markets
|
||||
|
||||
|
||||
@pytest.mark.parametrize("margin_mode", ["isolated", "cross"])
|
||||
def test_hyperliquid_dry_run_liquidation_price(default_conf, markets_hip3, 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 = {
|
||||
"BTC/USDC:USDC": {"limits": {"leverage": {"max": 50}}},
|
||||
"ETH/USDC:USDC": {"limits": {"leverage": {"max": 50}}},
|
||||
"SOL/USDC:USDC": {"limits": {"leverage": {"max": 20}}},
|
||||
"DOGE/USDC:USDC": {"limits": {"leverage": {"max": 20}}},
|
||||
}
|
||||
|
||||
positions = [
|
||||
{
|
||||
"symbol": "ETH/USDC:USDC",
|
||||
@@ -277,14 +382,41 @@ def test_hyperliquid_dry_run_liquidation_price(default_conf, mocker):
|
||||
"leverage": 3.0,
|
||||
"liquidationPrice": 45236.52992613,
|
||||
},
|
||||
{
|
||||
"symbol": "XYZ-AAPL/USDC:USDC",
|
||||
"entryPrice": 250.0,
|
||||
"side": "long",
|
||||
"contracts": 0.5,
|
||||
"collateral": 25.0,
|
||||
"leverage": 5.0,
|
||||
"liquidationPrice": 210.5263157894737,
|
||||
},
|
||||
{
|
||||
"symbol": "XYZ-GOOGL/USDC:USDC",
|
||||
"entryPrice": 190.0,
|
||||
"side": "short",
|
||||
"contracts": 0.5,
|
||||
"collateral": 9.5,
|
||||
"leverage": 10.0,
|
||||
"liquidationPrice": 199.04761904761904,
|
||||
},
|
||||
{
|
||||
"symbol": "XYZ-TSLA/USDC:USDC",
|
||||
"entryPrice": 350.0,
|
||||
"side": "long",
|
||||
"contracts": 1.0,
|
||||
"collateral": 50.0,
|
||||
"leverage": 7.0,
|
||||
"liquidationPrice": 315.7894736842105,
|
||||
},
|
||||
]
|
||||
|
||||
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
|
||||
api_mock.markets = markets_hip3
|
||||
exchange = get_patched_exchange(
|
||||
mocker, default_conf, api_mock, exchange="hyperliquid", mock_markets=False
|
||||
)
|
||||
@@ -299,51 +431,93 @@ 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)
|
||||
exchange = get_patched_exchange(mocker, default_conf, exchange="hyperliquid")
|
||||
exchange._fetch_and_calculate_funding_fees = MagicMock()
|
||||
|
||||
# Spot mode - no funding fees
|
||||
exchange.get_funding_fees("BTC/USDC:USDC", 1, False, now)
|
||||
assert exchange._fetch_and_calculate_funding_fees.call_count == 0
|
||||
|
||||
default_conf["trading_mode"] = "futures"
|
||||
default_conf["margin_mode"] = "isolated"
|
||||
default_conf["exchange"]["hip3_dexes"] = ["xyz", "vntl"]
|
||||
|
||||
# Mock validate_config to skip validation
|
||||
mocker.patch("freqtrade.exchange.hyperliquid.Hyperliquid.validate_config")
|
||||
|
||||
exchange = get_patched_exchange(mocker, default_conf, exchange="hyperliquid")
|
||||
exchange._fetch_and_calculate_funding_fees = MagicMock()
|
||||
exchange.get_funding_fees("BTC/USDC:USDC", 1, False, now)
|
||||
|
||||
# Normal market
|
||||
exchange.get_funding_fees("BTC/USDC:USDC", 1, False, now)
|
||||
assert exchange._fetch_and_calculate_funding_fees.call_count == 1
|
||||
|
||||
# HIP-3 XYZ market
|
||||
exchange._fetch_and_calculate_funding_fees.reset_mock()
|
||||
exchange.get_funding_fees("XYZ-TSLA/USDC:USDC", 1, False, now)
|
||||
assert exchange._fetch_and_calculate_funding_fees.call_count == 1
|
||||
|
||||
# HIP-3 VNTL market
|
||||
exchange._fetch_and_calculate_funding_fees.reset_mock()
|
||||
exchange.get_funding_fees("VNTL-SPACEX/USDH:USDH", 1, True, now)
|
||||
assert exchange._fetch_and_calculate_funding_fees.call_count == 1
|
||||
|
||||
|
||||
def test_hyperliquid_get_max_leverage(default_conf, mocker):
|
||||
markets = {
|
||||
"BTC/USDC:USDC": {"limits": {"leverage": {"max": 50}}},
|
||||
"ETH/USDC:USDC": {"limits": {"leverage": {"max": 50}}},
|
||||
"SOL/USDC:USDC": {"limits": {"leverage": {"max": 20}}},
|
||||
"DOGE/USDC:USDC": {"limits": {"leverage": {"max": 20}}},
|
||||
}
|
||||
def test_hyperliquid_get_max_leverage(default_conf, mocker, markets_hip3):
|
||||
exchange = get_patched_exchange(mocker, default_conf, exchange="hyperliquid")
|
||||
assert exchange.get_max_leverage("BTC/USDC:USDC", 1) == 1.0
|
||||
|
||||
default_conf["trading_mode"] = "futures"
|
||||
default_conf["margin_mode"] = "isolated"
|
||||
exchange = get_patched_exchange(mocker, default_conf, exchange="hyperliquid")
|
||||
mocker.patch.multiple(
|
||||
EXMS,
|
||||
markets=PropertyMock(return_value=markets),
|
||||
)
|
||||
default_conf["exchange"]["hip3_dexes"] = ["xyz", "vntl"]
|
||||
|
||||
# Mock validate_config to skip validation
|
||||
mocker.patch("freqtrade.exchange.hyperliquid.Hyperliquid.validate_config")
|
||||
|
||||
exchange = get_patched_exchange(mocker, default_conf, exchange="hyperliquid")
|
||||
mocker.patch.multiple(EXMS, markets=PropertyMock(return_value=markets_hip3))
|
||||
|
||||
# Normal markets
|
||||
assert exchange.get_max_leverage("BTC/USDC:USDC", 1) == 50
|
||||
assert exchange.get_max_leverage("ETH/USDC:USDC", 20) == 50
|
||||
assert exchange.get_max_leverage("SOL/USDC:USDC", 50) == 20
|
||||
assert exchange.get_max_leverage("DOGE/USDC:USDC", 3) == 20
|
||||
|
||||
# HIP-3 markets
|
||||
assert exchange.get_max_leverage("XYZ-TSLA/USDC:USDC", 1) == 10
|
||||
assert exchange.get_max_leverage("XYZ-NVDA/USDC:USDC", 5) == 10
|
||||
assert exchange.get_max_leverage("VNTL-SPACEX/USDH:USDH", 2) == 3
|
||||
assert exchange.get_max_leverage("VNTL-ANTHROPIC/USDH:USDH", 1) == 3
|
||||
|
||||
|
||||
def test_hyperliquid__lev_prep(default_conf, mocker):
|
||||
api_mock = MagicMock()
|
||||
@@ -360,25 +534,59 @@ def test_hyperliquid__lev_prep(default_conf, mocker):
|
||||
|
||||
default_conf["trading_mode"] = "futures"
|
||||
default_conf["margin_mode"] = "isolated"
|
||||
default_conf["exchange"]["hip3_dexes"] = ["xyz", "vntl"]
|
||||
|
||||
# Mock validate_config to skip validation
|
||||
mocker.patch("freqtrade.exchange.hyperliquid.Hyperliquid.validate_config")
|
||||
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange="hyperliquid")
|
||||
exchange._lev_prep("BTC/USDC:USDC", 3.2, "buy")
|
||||
|
||||
# Normal market
|
||||
exchange._lev_prep("BTC/USDC:USDC", 3.2, "buy")
|
||||
assert api_mock.set_margin_mode.call_count == 1
|
||||
api_mock.set_margin_mode.assert_called_with("isolated", "BTC/USDC:USDC", {"leverage": 3})
|
||||
|
||||
api_mock.reset_mock()
|
||||
|
||||
exchange._lev_prep("BTC/USDC:USDC", 19.99, "sell")
|
||||
|
||||
assert api_mock.set_margin_mode.call_count == 1
|
||||
api_mock.set_margin_mode.assert_called_with("isolated", "BTC/USDC:USDC", {"leverage": 19})
|
||||
|
||||
# HIP-3 XYZ market
|
||||
api_mock.reset_mock()
|
||||
exchange._lev_prep("XYZ-TSLA/USDC:USDC", 5.7, "buy")
|
||||
assert api_mock.set_margin_mode.call_count == 1
|
||||
api_mock.set_margin_mode.assert_called_with("isolated", "XYZ-TSLA/USDC:USDC", {"leverage": 5})
|
||||
|
||||
def test_hyperliquid_fetch_order(default_conf_usdt, mocker):
|
||||
api_mock.reset_mock()
|
||||
exchange._lev_prep("XYZ-TSLA/USDC:USDC", 10.0, "sell")
|
||||
assert api_mock.set_margin_mode.call_count == 1
|
||||
api_mock.set_margin_mode.assert_called_with("isolated", "XYZ-TSLA/USDC:USDC", {"leverage": 10})
|
||||
|
||||
# HIP-3 VNTL market
|
||||
api_mock.reset_mock()
|
||||
exchange._lev_prep("VNTL-SPACEX/USDH:USDH", 2.5, "buy")
|
||||
assert api_mock.set_margin_mode.call_count == 1
|
||||
api_mock.set_margin_mode.assert_called_with(
|
||||
"isolated", "VNTL-SPACEX/USDH:USDH", {"leverage": 2}
|
||||
)
|
||||
|
||||
api_mock.reset_mock()
|
||||
exchange._lev_prep("VNTL-ANTHROPIC/USDH:USDH", 3.0, "sell")
|
||||
assert api_mock.set_margin_mode.call_count == 1
|
||||
api_mock.set_margin_mode.assert_called_with(
|
||||
"isolated", "VNTL-ANTHROPIC/USDH:USDH", {"leverage": 3}
|
||||
)
|
||||
|
||||
|
||||
def test_hyperliquid_fetch_order(default_conf_usdt, mocker, markets_hip3):
|
||||
default_conf_usdt["dry_run"] = False
|
||||
default_conf_usdt["trading_mode"] = "futures"
|
||||
default_conf_usdt["margin_mode"] = "isolated"
|
||||
default_conf_usdt["exchange"]["hip3_dexes"] = ["xyz", "vntl"]
|
||||
|
||||
api_mock = MagicMock()
|
||||
|
||||
# Test with normal market
|
||||
api_mock.fetch_order = MagicMock(
|
||||
return_value={
|
||||
"id": "12345",
|
||||
@@ -410,9 +618,293 @@ def test_hyperliquid_fetch_order(default_conf_usdt, mocker):
|
||||
},
|
||||
],
|
||||
)
|
||||
exchange = get_patched_exchange(mocker, default_conf_usdt, api_mock, exchange="hyperliquid")
|
||||
mocker.patch("freqtrade.exchange.hyperliquid.Hyperliquid.validate_config")
|
||||
exchange = get_patched_exchange(
|
||||
mocker, default_conf_usdt, api_mock, exchange="hyperliquid", mock_markets=markets_hip3
|
||||
)
|
||||
o = exchange.fetch_order("12345", "ETH/USDC:USDC")
|
||||
# Uses weighted average
|
||||
assert o["average"] == 1500
|
||||
|
||||
assert gtfo_mock.call_count == 1
|
||||
|
||||
# Test with HIP-3 XYZ market
|
||||
api_mock.fetch_order = MagicMock(
|
||||
return_value={
|
||||
"id": "67890",
|
||||
"symbol": "XYZ-TSLA/USDC:USDC",
|
||||
"status": "closed",
|
||||
"filled": 2.5,
|
||||
"average": None,
|
||||
"timestamp": 1630000100,
|
||||
}
|
||||
)
|
||||
gtfo_mock.reset_mock()
|
||||
gtfo_mock.return_value = [
|
||||
{
|
||||
"order_id": "67890",
|
||||
"price": 250,
|
||||
"amount": 1.5,
|
||||
"filled": 1.5,
|
||||
"remaining": 0,
|
||||
},
|
||||
{
|
||||
"order_id": "67890",
|
||||
"price": 260,
|
||||
"amount": 1.0,
|
||||
"filled": 1.0,
|
||||
"remaining": 0,
|
||||
},
|
||||
]
|
||||
|
||||
o = exchange.fetch_order("67890", "XYZ-TSLA/USDC:USDC")
|
||||
# Weighted average: (250*1.5 + 260*1.0) / 2.5 = 254
|
||||
assert o["average"] == 254
|
||||
assert gtfo_mock.call_count == 1
|
||||
|
||||
# Test with HIP-3 VNTL market
|
||||
api_mock.fetch_order = MagicMock(
|
||||
return_value={
|
||||
"id": "11111",
|
||||
"symbol": "VNTL-SPACEX/USDH:USDH",
|
||||
"status": "closed",
|
||||
"filled": 5.0,
|
||||
"average": None,
|
||||
"timestamp": 1630000200,
|
||||
}
|
||||
)
|
||||
gtfo_mock.reset_mock()
|
||||
gtfo_mock.return_value = [
|
||||
{
|
||||
"order_id": "11111",
|
||||
"price": 100,
|
||||
"amount": 3.0,
|
||||
"filled": 3.0,
|
||||
"remaining": 0,
|
||||
},
|
||||
{
|
||||
"order_id": "11111",
|
||||
"price": 105,
|
||||
"amount": 2.0,
|
||||
"filled": 2.0,
|
||||
"remaining": 0,
|
||||
},
|
||||
]
|
||||
|
||||
o = exchange.fetch_order("11111", "VNTL-SPACEX/USDH:USDH")
|
||||
assert o["average"] == 102
|
||||
assert gtfo_mock.call_count == 1
|
||||
|
||||
|
||||
def test_hyperliquid_hip3_config_validation(default_conf_usdt, mocker, markets_hip3):
|
||||
"""Test HIP-3 DEX configuration validation."""
|
||||
|
||||
api_mock = MagicMock()
|
||||
default_conf_usdt["stake_currency"] = "USDC"
|
||||
|
||||
# Futures mode, no dex configured
|
||||
default_conf_copy = deepcopy(default_conf_usdt)
|
||||
default_conf_copy["trading_mode"] = "futures"
|
||||
default_conf_copy["margin_mode"] = "isolated"
|
||||
exchange = get_patched_exchange(
|
||||
mocker, default_conf_copy, api_mock, exchange="hyperliquid", mock_markets=markets_hip3
|
||||
)
|
||||
exchange.validate_config(default_conf_copy)
|
||||
|
||||
# Not in futures mode - no dex configured - no error
|
||||
get_patched_exchange(
|
||||
mocker, default_conf_usdt, api_mock, exchange="hyperliquid", mock_markets=markets_hip3
|
||||
)
|
||||
# Not in futures mode
|
||||
default_conf_usdt["exchange"]["hip3_dexes"] = ["xyz"]
|
||||
with pytest.raises(
|
||||
ConfigurationError, match=r"HIP-3 DEXes are only supported in FUTURES trading mode\."
|
||||
):
|
||||
get_patched_exchange(
|
||||
mocker, default_conf_usdt, api_mock, exchange="hyperliquid", mock_markets=markets_hip3
|
||||
)
|
||||
# Valid single DEX
|
||||
default_conf_usdt["trading_mode"] = "futures"
|
||||
default_conf_usdt["margin_mode"] = "isolated"
|
||||
default_conf_usdt["exchange"]["hip3_dexes"] = ["xyz"]
|
||||
exchange = get_patched_exchange(
|
||||
mocker, default_conf_usdt, api_mock, exchange="hyperliquid", mock_markets=markets_hip3
|
||||
)
|
||||
assert exchange._get_configured_hip3_dexes() == ["xyz"]
|
||||
|
||||
# Invalid DEX
|
||||
default_conf_usdt["exchange"]["hip3_dexes"] = ["invalid_dex"]
|
||||
with pytest.raises(ConfigurationError, match="Invalid HIP-3 DEXes configured"):
|
||||
exchange = get_patched_exchange(
|
||||
mocker, default_conf_usdt, api_mock, exchange="hyperliquid", mock_markets=markets_hip3
|
||||
)
|
||||
exchange.validate_config(default_conf_usdt)
|
||||
|
||||
# Mix of valid and invalid DEX
|
||||
default_conf_usdt["exchange"]["hip3_dexes"] = ["xyz", "invalid_dex"]
|
||||
with pytest.raises(ConfigurationError, match="Invalid HIP-3 DEXes configured"):
|
||||
exchange = get_patched_exchange(
|
||||
mocker, default_conf_usdt, api_mock, exchange="hyperliquid", mock_markets=markets_hip3
|
||||
)
|
||||
exchange.validate_config(default_conf_usdt)
|
||||
|
||||
default_conf_usdt["margin_mode"] = "cross"
|
||||
with pytest.raises(ConfigurationError, match="HIP-3 DEXes require 'isolated' margin mode"):
|
||||
exchange = get_patched_exchange(
|
||||
mocker, default_conf_usdt, api_mock, exchange="hyperliquid", mock_markets=markets_hip3
|
||||
)
|
||||
exchange.validate_config(default_conf_usdt)
|
||||
|
||||
|
||||
def test_hyperliquid_get_balances_hip3(default_conf, mocker, caplog, markets_hip3):
|
||||
"""Test balance fetching from HIP-3 DEXes."""
|
||||
api_mock = MagicMock()
|
||||
|
||||
api_mock.load_markets = get_mock_coro()
|
||||
|
||||
# Mock balance responses
|
||||
default_balance = {"USDC": {"free": 1000, "used": 0, "total": 1000}}
|
||||
xyz_balance = {"USDC": {"free": 0, "used": 600, "total": 600}}
|
||||
vntl_balance = {"USDH": {"free": 0, "used": 300, "total": 300}}
|
||||
|
||||
def fetch_balance_side_effect(params=None):
|
||||
if params and params.get("dex") == "xyz":
|
||||
return xyz_balance
|
||||
elif params and params.get("dex") == "vntl":
|
||||
return vntl_balance
|
||||
elif params and params.get("dex") == "flx":
|
||||
raise Exception("FLX DEX error")
|
||||
return default_balance
|
||||
|
||||
api_mock.fetch_balance = MagicMock(side_effect=fetch_balance_side_effect)
|
||||
|
||||
# Test with two HIP-3 DEXes
|
||||
default_conf["exchange"]["hip3_dexes"] = ["xyz", "vntl", "flx"]
|
||||
default_conf["trading_mode"] = "futures"
|
||||
default_conf["margin_mode"] = "isolated"
|
||||
mocker.patch("freqtrade.exchange.hyperliquid.Hyperliquid.validate_config")
|
||||
exchange = get_patched_exchange(
|
||||
mocker, default_conf, api_mock, exchange="hyperliquid", mock_markets=markets_hip3
|
||||
)
|
||||
|
||||
balances = exchange.get_balances()
|
||||
|
||||
# Should have combined balances
|
||||
assert balances["USDC"]["free"] == 1000
|
||||
assert balances["USDC"]["used"] == 600
|
||||
assert balances["USDC"]["total"] == 1600
|
||||
assert balances["USDH"]["free"] == 0
|
||||
assert balances["USDH"]["used"] == 300
|
||||
assert balances["USDH"]["total"] == 300
|
||||
|
||||
assert api_mock.fetch_balance.call_count == 4
|
||||
assert log_has_re("Could not fetch balance for HIP-3 DEX.*", caplog)
|
||||
|
||||
|
||||
def test_hyperliquid_fetch_positions_hip3(default_conf, mocker, caplog, markets_hip3):
|
||||
"""Test position fetching from HIP-3 DEXes."""
|
||||
api_mock = MagicMock()
|
||||
|
||||
# Mock position responses
|
||||
default_positions = [{"symbol": "BTC/USDC:USDC", "contracts": 0.5}]
|
||||
xyz_positions = [{"symbol": "XYZ-AAPL/USDC:USDC", "contracts": 10}]
|
||||
vntl_positions = [{"symbol": "VNTL-SPACEX/USDH:USDH", "contracts": 5}]
|
||||
|
||||
def fetch_positions_side_effect(symbols=None, params=None):
|
||||
if params and params.get("dex") == "xyz":
|
||||
return xyz_positions
|
||||
elif params and params.get("dex") == "vntl":
|
||||
return vntl_positions
|
||||
elif params and params.get("dex") == "flx":
|
||||
raise Exception("FLX DEX error")
|
||||
return default_positions
|
||||
|
||||
positions_mock = MagicMock(side_effect=fetch_positions_side_effect)
|
||||
|
||||
default_conf["trading_mode"] = "futures"
|
||||
default_conf["margin_mode"] = "isolated"
|
||||
default_conf["exchange"]["hip3_dexes"] = ["xyz", "vntl", "flx"]
|
||||
|
||||
mocker.patch("freqtrade.exchange.hyperliquid.Hyperliquid.validate_config")
|
||||
exchange = get_patched_exchange(
|
||||
mocker, default_conf, api_mock, exchange="hyperliquid", mock_markets=markets_hip3
|
||||
)
|
||||
|
||||
# Mock super().fetch_positions() to return default positions
|
||||
mocker.patch(f"{EXMS}.fetch_positions", positions_mock)
|
||||
|
||||
positions = exchange.fetch_positions()
|
||||
|
||||
assert log_has_re("Could not fetch positions from HIP-3 .*", caplog)
|
||||
|
||||
# Should have all positions combined (default + HIP-3)
|
||||
assert len(positions) == 3
|
||||
assert any(p["symbol"] == "BTC/USDC:USDC" for p in positions)
|
||||
assert any(p["symbol"] == "XYZ-AAPL/USDC:USDC" for p in positions)
|
||||
assert any(p["symbol"] == "VNTL-SPACEX/USDH:USDH" for p in positions)
|
||||
|
||||
# Verify API calls (xyz + vntl, default is mocked separately)
|
||||
assert positions_mock.call_count == 4
|
||||
|
||||
|
||||
def test_hyperliquid_market_is_tradable(default_conf_usdt, mocker, markets_hip3):
|
||||
"""Test market_is_tradable filters HIP-3 markets correctly."""
|
||||
default_conf_usdt["stake_currency"] = "USDC"
|
||||
default_conf_usdt["trading_mode"] = "futures"
|
||||
default_conf_usdt["margin_mode"] = "isolated"
|
||||
api_mock = MagicMock()
|
||||
api_mock.load_markets = get_mock_coro(return_value=markets_hip3)
|
||||
api_mock.markets = markets_hip3
|
||||
# Mock parent call - we only want to test hyperliquid specifics here.
|
||||
mocker.patch(f"{EXMS}.market_is_tradable", return_value=True)
|
||||
|
||||
# Test 1: No HIP-3 DEXes configured - only default markets tradable
|
||||
default_conf_usdt["exchange"]["hip3_dexes"] = []
|
||||
exchange = get_patched_exchange(
|
||||
mocker, default_conf_usdt, api_mock, exchange="hyperliquid", mock_markets=False
|
||||
)
|
||||
|
||||
assert exchange.market_is_tradable(markets_hip3["BTC/USDC:USDC"]) is True
|
||||
assert exchange.market_is_tradable(markets_hip3["ETH/USDC:USDC"]) is True
|
||||
assert exchange.market_is_tradable(markets_hip3["XYZ-AAPL/USDC:USDC"]) is False
|
||||
assert exchange.market_is_tradable(markets_hip3["XYZ-TSLA/USDC:USDC"]) is False
|
||||
assert exchange.market_is_tradable(markets_hip3["VNTL-SPACEX/USDH:USDH"]) is False
|
||||
assert exchange.market_is_tradable(markets_hip3["FLX-TOKEN/USDC:USDC"]) is False
|
||||
|
||||
# Test 2: Only 'xyz' configured - default + xyz markets tradable
|
||||
default_conf_usdt["exchange"]["hip3_dexes"] = ["xyz"]
|
||||
exchange = get_patched_exchange(
|
||||
mocker, default_conf_usdt, api_mock, exchange="hyperliquid", mock_markets=False
|
||||
)
|
||||
|
||||
assert exchange.market_is_tradable(markets_hip3["BTC/USDC:USDC"]) is True
|
||||
assert exchange.market_is_tradable(markets_hip3["ETH/USDC:USDC"]) is True
|
||||
assert exchange.market_is_tradable(markets_hip3["XYZ-AAPL/USDC:USDC"]) is True
|
||||
assert exchange.market_is_tradable(markets_hip3["XYZ-TSLA/USDC:USDC"]) is True
|
||||
assert exchange.market_is_tradable(markets_hip3["VNTL-SPACEX/USDH:USDH"]) is False
|
||||
assert exchange.market_is_tradable(markets_hip3["FLX-TOKEN/USDC:USDC"]) is False
|
||||
|
||||
# Test 3: 'xyz' and 'vntl' configured - default + xyz + vntl markets tradable
|
||||
default_conf_usdt["exchange"]["hip3_dexes"] = ["xyz", "flx"]
|
||||
exchange = get_patched_exchange(
|
||||
mocker, default_conf_usdt, api_mock, exchange="hyperliquid", mock_markets=False
|
||||
)
|
||||
|
||||
assert exchange.market_is_tradable(markets_hip3["BTC/USDC:USDC"]) is True
|
||||
assert exchange.market_is_tradable(markets_hip3["ETH/USDC:USDC"]) is True
|
||||
assert exchange.market_is_tradable(markets_hip3["XYZ-AAPL/USDC:USDC"]) is True
|
||||
assert exchange.market_is_tradable(markets_hip3["XYZ-TSLA/USDC:USDC"]) is True
|
||||
assert exchange.market_is_tradable(markets_hip3["VNTL-SPACEX/USDH:USDH"]) is False
|
||||
assert exchange.market_is_tradable(markets_hip3["FLX-TOKEN/USDC:USDC"]) is True
|
||||
|
||||
# Use USDH stake currency to enable VNTL markets
|
||||
default_conf_usdt["exchange"]["hip3_dexes"] = ["vntl"]
|
||||
default_conf_usdt["stake_currency"] = "USDH"
|
||||
exchange = get_patched_exchange(
|
||||
mocker, default_conf_usdt, api_mock, exchange="hyperliquid", mock_markets=False
|
||||
)
|
||||
assert exchange.market_is_tradable(markets_hip3["BTC/USDC:USDC"]) is True
|
||||
assert exchange.market_is_tradable(markets_hip3["ETH/USDC:USDC"]) is True
|
||||
assert exchange.market_is_tradable(markets_hip3["XYZ-AAPL/USDC:USDC"]) is False
|
||||
assert exchange.market_is_tradable(markets_hip3["XYZ-TSLA/USDC:USDC"]) is False
|
||||
assert exchange.market_is_tradable(markets_hip3["VNTL-SPACEX/USDH:USDH"]) is True
|
||||
assert exchange.market_is_tradable(markets_hip3["FLX-TOKEN/USDC:USDC"]) is False
|
||||
|
||||
@@ -20,7 +20,7 @@ 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) == 300
|
||||
assert exchange.ohlcv_candle_limit(timeframe, CandleType.MARK) == 100
|
||||
assert exchange.ohlcv_candle_limit(timeframe, CandleType.FUNDING_RATE) == 100
|
||||
|
||||
assert exchange.ohlcv_candle_limit(timeframe, CandleType.SPOT, start_time) == 300
|
||||
@@ -36,7 +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
|
||||
assert exchange.ohlcv_candle_limit(timeframe, CandleType.MARK, one_call) == 100
|
||||
|
||||
one_call = int(
|
||||
(
|
||||
@@ -661,14 +661,14 @@ def test_stoploss_adjust_okx(mocker, default_conf, sl1, sl2, sl3, side):
|
||||
|
||||
def test_stoploss_cancel_okx(mocker, default_conf):
|
||||
exchange = get_patched_exchange(mocker, default_conf, exchange="okx")
|
||||
|
||||
exchange.cancel_order = MagicMock()
|
||||
co_mock = mocker.patch.object(exchange, "cancel_order", autospec=True)
|
||||
|
||||
exchange.cancel_stoploss_order("1234", "ETH/USDT")
|
||||
assert exchange.cancel_order.call_count == 1
|
||||
assert exchange.cancel_order.call_args_list[0][1]["order_id"] == "1234"
|
||||
assert exchange.cancel_order.call_args_list[0][1]["pair"] == "ETH/USDT"
|
||||
assert exchange.cancel_order.call_args_list[0][1]["params"] == {"stop": True}
|
||||
assert co_mock.call_count == 1
|
||||
args, _ = co_mock.call_args
|
||||
assert args[0] == "1234"
|
||||
assert args[1] == "ETH/USDT"
|
||||
assert args[2] == {"stop": True}
|
||||
|
||||
|
||||
def test__get_stop_params_okx(mocker, default_conf):
|
||||
|
||||
@@ -153,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": {
|
||||
@@ -161,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},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -418,6 +422,10 @@ EXCHANGES = {
|
||||
"hasQuoteVolume": True,
|
||||
"timeframe": "1h",
|
||||
"candle_count": 1000,
|
||||
"futures": True,
|
||||
"futures_pair": "BTC/USDT:USDT",
|
||||
"leverage_tiers_public": True,
|
||||
"leverage_in_spot_market": True,
|
||||
},
|
||||
"coinex": {
|
||||
"pair": "BTC/USDT",
|
||||
@@ -427,14 +435,13 @@ EXCHANGES = {
|
||||
"candle_count": 1000,
|
||||
"orderbook_max_entries": 50,
|
||||
},
|
||||
# TODO: re-enable htx once certificates work again
|
||||
# "htx": {
|
||||
# "pair": "ETH/BTC",
|
||||
# "stake_currency": "BTC",
|
||||
# "hasQuoteVolume": True,
|
||||
# "timeframe": "1h",
|
||||
# "candle_count": 1000,
|
||||
# },
|
||||
"htx": {
|
||||
"pair": "ETH/BTC",
|
||||
"stake_currency": "BTC",
|
||||
"hasQuoteVolume": True,
|
||||
"timeframe": "1h",
|
||||
"candle_count": 1000,
|
||||
},
|
||||
"bitvavo": {
|
||||
"pair": "BTC/EUR",
|
||||
"stake_currency": "EUR",
|
||||
@@ -507,7 +514,7 @@ EXCHANGES = {
|
||||
],
|
||||
},
|
||||
"hyperliquid": {
|
||||
"pair": "UBTC/USDC",
|
||||
"pair": "BTC/USDC",
|
||||
"stake_currency": "USDC",
|
||||
"hasQuoteVolume": False,
|
||||
"timeframe": "30m",
|
||||
@@ -515,6 +522,8 @@ EXCHANGES = {
|
||||
"candle_count": 5000,
|
||||
"orderbook_max_entries": 20,
|
||||
"futures_pair": "BTC/USDC:USDC",
|
||||
# Assert that HIP3 pairs are fetched as part of load_markets
|
||||
"futures_alt_pairs": ["XYZ-NVDA/USDC:USDC", "VNTL-ANTHROPIC/USDH:USDH"],
|
||||
"hasQuoteVolumeFutures": True,
|
||||
"leverage_tiers_public": False,
|
||||
"leverage_in_spot_market": False,
|
||||
@@ -577,10 +586,7 @@ def get_futures_exchange(exchange_name, exchange_conf, class_mocker):
|
||||
|
||||
class_mocker.patch("freqtrade.exchange.binance.Binance.fill_leverage_tiers")
|
||||
class_mocker.patch(f"{EXMS}.fetch_trading_fees")
|
||||
class_mocker.patch("freqtrade.exchange.okx.Okx.additional_exchange_init")
|
||||
class_mocker.patch("freqtrade.exchange.binance.Binance.additional_exchange_init")
|
||||
class_mocker.patch("freqtrade.exchange.bybit.Bybit.additional_exchange_init")
|
||||
class_mocker.patch("freqtrade.exchange.gate.Gate.additional_exchange_init")
|
||||
class_mocker.patch(f"{EXMS}.ft_additional_exchange_init")
|
||||
class_mocker.patch(f"{EXMS}.load_cached_leverage_tiers", return_value=None)
|
||||
class_mocker.patch(f"{EXMS}.cache_leverage_tiers")
|
||||
|
||||
@@ -589,7 +595,7 @@ def get_futures_exchange(exchange_name, exchange_conf, class_mocker):
|
||||
|
||||
@pytest.fixture(params=EXCHANGES, scope="class")
|
||||
def exchange(request, exchange_conf, class_mocker):
|
||||
class_mocker.patch("freqtrade.exchange.bybit.Bybit.additional_exchange_init")
|
||||
class_mocker.patch(f"{EXMS}.ft_additional_exchange_init")
|
||||
exchange, name = get_exchange(request.param, exchange_conf)
|
||||
yield exchange, name
|
||||
exchange.close()
|
||||
|
||||
@@ -11,7 +11,7 @@ import pytest
|
||||
|
||||
from freqtrade.enums import CandleType
|
||||
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_prev_date
|
||||
from freqtrade.exchange.exchange import timeframe_to_msecs
|
||||
from freqtrade.exchange.exchange import Exchange, timeframe_to_msecs
|
||||
from freqtrade.util import dt_floor_day, dt_now, dt_ts
|
||||
from tests.exchange_online.conftest import EXCHANGE_FIXTURE_TYPE, EXCHANGES
|
||||
|
||||
@@ -67,12 +67,14 @@ class TestCCXTExchange:
|
||||
def test_load_markets_futures(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
|
||||
exchange, exchangename = exchange_futures
|
||||
pair = EXCHANGES[exchangename]["pair"]
|
||||
pair = EXCHANGES[exchangename].get("futures_pair", pair)
|
||||
pair1 = EXCHANGES[exchangename].get("futures_pair", pair)
|
||||
alternative_pairs = EXCHANGES[exchangename].get("futures_alt_pairs", [])
|
||||
markets = exchange.markets
|
||||
assert pair in markets
|
||||
assert isinstance(markets[pair], dict)
|
||||
for pair in [pair1] + alternative_pairs:
|
||||
assert pair in markets, f"Futures pair {pair} not found in markets"
|
||||
assert isinstance(markets[pair], dict)
|
||||
|
||||
assert exchange.market_is_future(markets[pair])
|
||||
assert exchange.market_is_future(markets[pair])
|
||||
|
||||
def test_ccxt_order_parse(self, exchange: EXCHANGE_FIXTURE_TYPE):
|
||||
exch, exchange_name = exchange
|
||||
@@ -270,11 +272,14 @@ class TestCCXTExchange:
|
||||
assert exch.klines(pair_tf).iloc[-1]["date"] >= timeframe_to_prev_date(timeframe, now)
|
||||
assert exch.klines(pair_tf)["date"].astype(int).iloc[0] // 1e6 == since_ms
|
||||
|
||||
def _ccxt__async_get_candle_history(self, exchange, pair, timeframe, candle_type, factor=0.9):
|
||||
def _ccxt__async_get_candle_history(
|
||||
self, exchange, pair: str, timeframe: str, candle_type: CandleType, factor: float = 0.9
|
||||
):
|
||||
timeframe_ms = timeframe_to_msecs(timeframe)
|
||||
timeframe_ms_8h = timeframe_to_msecs("8h")
|
||||
now = timeframe_to_prev_date(timeframe, datetime.now(UTC))
|
||||
for offset in (360, 120, 30, 10, 5, 2):
|
||||
since = now - timedelta(days=offset)
|
||||
for offset_days in (360, 120, 30, 10, 5, 2):
|
||||
since = now - timedelta(days=offset_days)
|
||||
since_ms = int(since.timestamp() * 1000)
|
||||
|
||||
res = exchange.loop.run_until_complete(
|
||||
@@ -289,8 +294,15 @@ class TestCCXTExchange:
|
||||
candles = res[3]
|
||||
candle_count = exchange.ohlcv_candle_limit(timeframe, candle_type, since_ms) * factor
|
||||
candle_count1 = (now.timestamp() * 1000 - since_ms) // timeframe_ms * factor
|
||||
assert len(candles) >= min(candle_count, candle_count1), (
|
||||
f"{len(candles)} < {candle_count} in {timeframe}, Offset: {offset} {factor}"
|
||||
# funding fees can be 1h or 8h - depending on pair and time.
|
||||
candle_count2 = (now.timestamp() * 1000 - since_ms) // timeframe_ms_8h * factor
|
||||
min_value = min(
|
||||
candle_count,
|
||||
candle_count1,
|
||||
candle_count2 if candle_type == CandleType.FUNDING_RATE else candle_count1,
|
||||
)
|
||||
assert len(candles) >= min_value, (
|
||||
f"{len(candles)} < {candle_count} in {timeframe} {offset_days=} {factor=}"
|
||||
)
|
||||
# Check if first-timeframe is either the start, or start + 1
|
||||
assert candles[0][0] == since_ms or (since_ms + timeframe_ms)
|
||||
@@ -309,6 +321,8 @@ class TestCCXTExchange:
|
||||
[
|
||||
CandleType.FUTURES,
|
||||
CandleType.FUNDING_RATE,
|
||||
CandleType.INDEX,
|
||||
CandleType.PREMIUMINDEX,
|
||||
CandleType.MARK,
|
||||
],
|
||||
)
|
||||
@@ -322,6 +336,10 @@ class TestCCXTExchange:
|
||||
timeframe = exchange._ft_has.get(
|
||||
"funding_fee_timeframe", exchange._ft_has["mark_ohlcv_timeframe"]
|
||||
)
|
||||
else:
|
||||
# never skip funding rate!
|
||||
if not exchange.check_candle_type_support(candle_type):
|
||||
pytest.skip(f"Exchange does not support candle type {candle_type}")
|
||||
self._ccxt__async_get_candle_history(
|
||||
exchange,
|
||||
pair=pair,
|
||||
@@ -337,6 +355,7 @@ class TestCCXTExchange:
|
||||
timeframe_ff = exchange._ft_has.get(
|
||||
"funding_fee_timeframe", exchange._ft_has["mark_ohlcv_timeframe"]
|
||||
)
|
||||
timeframe_ff_8h = "8h"
|
||||
pair_tf = (pair, timeframe_ff, CandleType.FUNDING_RATE)
|
||||
|
||||
funding_ohlcv = exchange.refresh_latest_ohlcv(
|
||||
@@ -350,14 +369,26 @@ class TestCCXTExchange:
|
||||
hour1 = timeframe_to_prev_date(timeframe_ff, this_hour - timedelta(minutes=1))
|
||||
hour2 = timeframe_to_prev_date(timeframe_ff, hour1 - timedelta(minutes=1))
|
||||
hour3 = timeframe_to_prev_date(timeframe_ff, hour2 - timedelta(minutes=1))
|
||||
val0 = rate[rate["date"] == this_hour].iloc[0]["open"]
|
||||
val1 = rate[rate["date"] == hour1].iloc[0]["open"]
|
||||
val2 = rate[rate["date"] == hour2].iloc[0]["open"]
|
||||
val3 = rate[rate["date"] == hour3].iloc[0]["open"]
|
||||
# Alternative 8h timeframe - funding fee timeframe is not stable.
|
||||
h8_this_hour = timeframe_to_prev_date(timeframe_ff_8h)
|
||||
h8_hour1 = timeframe_to_prev_date(timeframe_ff_8h, h8_this_hour - timedelta(minutes=1))
|
||||
h8_hour2 = timeframe_to_prev_date(timeframe_ff_8h, h8_hour1 - timedelta(minutes=1))
|
||||
h8_hour3 = timeframe_to_prev_date(timeframe_ff_8h, h8_hour2 - timedelta(minutes=1))
|
||||
row0 = rate.iloc[-1]
|
||||
row1 = rate.iloc[-2]
|
||||
row2 = rate.iloc[-3]
|
||||
row3 = rate.iloc[-4]
|
||||
|
||||
assert row0["date"] == this_hour or row0["date"] == h8_this_hour
|
||||
assert row1["date"] == hour1 or row1["date"] == h8_hour1
|
||||
assert row2["date"] == hour2 or row2["date"] == h8_hour2
|
||||
assert row3["date"] == hour3 or row3["date"] == h8_hour3
|
||||
|
||||
# Test For last 4 hours
|
||||
# Avoids random test-failure when funding-fees are 0 for a few hours.
|
||||
assert val0 != 0.0 or val1 != 0.0 or val2 != 0.0 or val3 != 0.0
|
||||
assert (
|
||||
row0["open"] != 0.0 or row1["open"] != 0.0 or row2["open"] != 0.0 or row3["open"] != 0.0
|
||||
)
|
||||
# We expect funding rates to be different from 0.0 - or moving around.
|
||||
assert (
|
||||
rate["open"].max() != 0.0
|
||||
@@ -369,7 +400,10 @@ class TestCCXTExchange:
|
||||
exchange, exchangename = exchange_futures
|
||||
pair = EXCHANGES[exchangename].get("futures_pair", EXCHANGES[exchangename]["pair"])
|
||||
since = int((datetime.now(UTC) - timedelta(days=5)).timestamp() * 1000)
|
||||
pair_tf = (pair, "1h", CandleType.MARK)
|
||||
candle_type = CandleType.from_string(
|
||||
exchange.get_option("mark_ohlcv_price", default=CandleType.MARK)
|
||||
)
|
||||
pair_tf = (pair, "1h", candle_type)
|
||||
|
||||
mark_ohlcv = exchange.refresh_latest_ohlcv([pair_tf], since_ms=since, drop_incomplete=False)
|
||||
|
||||
@@ -422,15 +456,23 @@ class TestCCXTExchange:
|
||||
trades_orig = nvspy.call_args_list[2][0][0]
|
||||
assert len(trades_orig[-1].get("info")) > len(trades_orig[-2].get("info"))
|
||||
|
||||
def test_ccxt_get_fee(self, exchange: EXCHANGE_FIXTURE_TYPE):
|
||||
exch, exchangename = exchange
|
||||
pair = EXCHANGES[exchangename]["pair"]
|
||||
def _ccxt_get_fee(self, exch: Exchange, pair: str):
|
||||
threshold = 0.01
|
||||
assert 0 < exch.get_fee(pair, "limit", "buy") < threshold
|
||||
assert 0 < exch.get_fee(pair, "limit", "sell") < threshold
|
||||
assert 0 < exch.get_fee(pair, "market", "buy") < threshold
|
||||
assert 0 < exch.get_fee(pair, "market", "sell") < threshold
|
||||
|
||||
def test_ccxt_get_fee_spot(self, exchange: EXCHANGE_FIXTURE_TYPE):
|
||||
exch, exchangename = exchange
|
||||
pair = EXCHANGES[exchangename]["pair"]
|
||||
self._ccxt_get_fee(exch, pair)
|
||||
|
||||
def test_ccxt_get_fee_futures(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
|
||||
exch, exchangename = exchange_futures
|
||||
pair = EXCHANGES[exchangename].get("futures_pair", EXCHANGES[exchangename]["pair"])
|
||||
self._ccxt_get_fee(exch, pair)
|
||||
|
||||
def test_ccxt_get_max_leverage_spot(self, exchange: EXCHANGE_FIXTURE_TYPE):
|
||||
spot, spot_name = exchange
|
||||
if spot:
|
||||
@@ -475,12 +517,13 @@ class TestCCXTExchange:
|
||||
for tier in pair_tiers:
|
||||
for key in ["maintenanceMarginRate", "minNotional", "maxNotional", "maxLeverage"]:
|
||||
assert key in tier
|
||||
assert tier[key] >= 0.0
|
||||
assert tier["maxNotional"] > tier["minNotional"]
|
||||
# maxNotional can be None (no limit)
|
||||
assert tier[key] is None or tier[key] >= 0.0
|
||||
assert tier["maxNotional"] is None or tier["maxNotional"] > tier["minNotional"]
|
||||
assert tier["maxLeverage"] <= oldLeverage
|
||||
assert tier["maintenanceMarginRate"] >= oldMaintenanceMarginRate
|
||||
assert tier["minNotional"] > oldminNotional
|
||||
assert tier["maxNotional"] > oldmaxNotional
|
||||
assert tier["maxNotional"] is None or tier["maxNotional"] > oldmaxNotional
|
||||
oldLeverage = tier["maxLeverage"]
|
||||
oldMaintenanceMarginRate = tier["maintenanceMarginRate"]
|
||||
oldminNotional = tier["minNotional"]
|
||||
|
||||
@@ -31,9 +31,6 @@ from tests.freqai.conftest import (
|
||||
def can_run_model(model: str) -> None:
|
||||
is_pytorch_model = "Reinforcement" in model or "PyTorch" in model
|
||||
|
||||
if is_arm() and "Catboost" in model:
|
||||
pytest.skip("CatBoost is not supported on ARM.")
|
||||
|
||||
if is_pytorch_model and is_mac():
|
||||
pytest.skip("Reinforcement learning / PyTorch module not available on intel based Mac OS.")
|
||||
|
||||
@@ -44,7 +41,6 @@ def can_run_model(model: str) -> None:
|
||||
("LightGBMRegressor", True, False, True, True, False, 0, 0),
|
||||
("XGBoostRegressor", False, True, False, True, False, 10, 0.05),
|
||||
("XGBoostRFRegressor", False, False, False, True, False, 0, 0),
|
||||
("CatboostRegressor", False, False, False, True, True, 0, 0),
|
||||
("PyTorchMLPRegressor", False, False, False, False, False, 0, 0),
|
||||
("PyTorchTransformerRegressor", False, False, False, False, False, 0, 0),
|
||||
("ReinforcementLearner", False, True, False, True, False, 0, 0),
|
||||
@@ -138,11 +134,10 @@ def test_extract_data_and_train_model_Standard(
|
||||
[
|
||||
("LightGBMRegressorMultiTarget", "freqai_test_multimodel_strat"),
|
||||
("XGBoostRegressorMultiTarget", "freqai_test_multimodel_strat"),
|
||||
("CatboostRegressorMultiTarget", "freqai_test_multimodel_strat"),
|
||||
("LightGBMClassifierMultiTarget", "freqai_test_multimodel_classifier_strat"),
|
||||
("CatboostClassifierMultiTarget", "freqai_test_multimodel_classifier_strat"),
|
||||
],
|
||||
)
|
||||
@pytest.mark.filterwarnings(r"ignore:.*__sklearn_tags__.*:DeprecationWarning")
|
||||
def test_extract_data_and_train_model_MultiTargets(mocker, freqai_conf, model, strat):
|
||||
can_run_model(model)
|
||||
|
||||
@@ -183,7 +178,6 @@ def test_extract_data_and_train_model_MultiTargets(mocker, freqai_conf, model, s
|
||||
"model",
|
||||
[
|
||||
"LightGBMClassifier",
|
||||
"CatboostClassifier",
|
||||
"XGBoostClassifier",
|
||||
"XGBoostRFClassifier",
|
||||
"SKLearnRandomForestClassifier",
|
||||
@@ -245,13 +239,11 @@ def test_extract_data_and_train_model_Classifiers(mocker, freqai_conf, model):
|
||||
[
|
||||
("LightGBMRegressor", 2, "freqai_test_strat"),
|
||||
("XGBoostRegressor", 2, "freqai_test_strat"),
|
||||
("CatboostRegressor", 2, "freqai_test_strat"),
|
||||
("PyTorchMLPRegressor", 2, "freqai_test_strat"),
|
||||
("PyTorchTransformerRegressor", 2, "freqai_test_strat"),
|
||||
("ReinforcementLearner", 3, "freqai_rl_test_strat"),
|
||||
("XGBoostClassifier", 2, "freqai_test_classifier"),
|
||||
("LightGBMClassifier", 2, "freqai_test_classifier"),
|
||||
("CatboostClassifier", 2, "freqai_test_classifier"),
|
||||
("PyTorchMLPClassifier", 2, "freqai_test_classifier"),
|
||||
],
|
||||
)
|
||||
|
||||
@@ -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])
|
||||
@@ -2536,9 +2548,9 @@ def test_manage_open_orders_exception(
|
||||
caplog.clear()
|
||||
freqtrade.manage_open_orders()
|
||||
assert log_has_re(
|
||||
r"Cannot query order for Trade\(id=1, pair=ADA/USDT, amount=30.00000000, "
|
||||
r"is_short=False, leverage=1.0, "
|
||||
r"open_rate=2.00000000, open_since="
|
||||
r"Cannot query order for Trade\(id=1, pair=ADA/USDT, amount=30, "
|
||||
r"is_short=False, leverage=1, "
|
||||
r"open_rate=2, open_since="
|
||||
f"{open_trade_usdt.open_date.strftime('%Y-%m-%d %H:%M:%S')}"
|
||||
r"\) due to Traceback \(most recent call last\):\n*",
|
||||
caplog,
|
||||
@@ -3080,7 +3092,7 @@ def test_execute_trade_exit_custom_exit_price(
|
||||
"exit_reason": "foo",
|
||||
"open_date": ANY,
|
||||
"close_date": ANY,
|
||||
"close_rate": ANY,
|
||||
"close_rate": 2.25, # the custom exit price
|
||||
"sub_trade": False,
|
||||
"cumulative_profit": 0.0,
|
||||
"stake_amount": pytest.approx(60),
|
||||
@@ -3739,8 +3751,8 @@ def test_get_real_amount_quote(
|
||||
# Amount is reduced by "fee"
|
||||
assert freqtrade.get_real_amount(trade, buy_order_fee, order_obj) == (amount * 0.001)
|
||||
assert log_has(
|
||||
"Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, is_short=False,"
|
||||
" leverage=1.0, open_rate=0.24544100, open_since=closed), fee=0.008.",
|
||||
"Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8, is_short=False,"
|
||||
" leverage=1, open_rate=0.245441, open_since=closed), fee=0.008.",
|
||||
caplog,
|
||||
)
|
||||
|
||||
@@ -3793,8 +3805,8 @@ def test_get_real_amount_no_trade(default_conf_usdt, buy_order_fee, caplog, mock
|
||||
# Amount is reduced by "fee"
|
||||
assert freqtrade.get_real_amount(trade, buy_order_fee, order_obj) is None
|
||||
assert log_has(
|
||||
"Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, "
|
||||
"is_short=False, leverage=1.0, open_rate=0.24544100, open_since=closed) failed: "
|
||||
"Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8, "
|
||||
"is_short=False, leverage=1, open_rate=0.245441, open_since=closed) failed: "
|
||||
"myTrade-dict empty found",
|
||||
caplog,
|
||||
)
|
||||
@@ -3813,8 +3825,8 @@ def test_get_real_amount_no_trade(default_conf_usdt, buy_order_fee, caplog, mock
|
||||
0,
|
||||
True,
|
||||
(
|
||||
"Fee for Trade Trade(id=None, pair=LTC/ETH, amount=8.00000000, is_short=False, "
|
||||
"leverage=1.0, open_rate=0.24544100, open_since=closed) [buy]: 0.00094518 BNB -"
|
||||
"Fee for Trade Trade(id=None, pair=LTC/ETH, amount=8, is_short=False, "
|
||||
"leverage=1, open_rate=0.245441, open_since=closed) [buy]: 0.00094518 BNB -"
|
||||
" rate: None"
|
||||
),
|
||||
),
|
||||
@@ -3824,8 +3836,8 @@ def test_get_real_amount_no_trade(default_conf_usdt, buy_order_fee, caplog, mock
|
||||
0.004,
|
||||
False,
|
||||
(
|
||||
"Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, "
|
||||
"is_short=False, leverage=1.0, open_rate=0.24544100, open_since=closed), fee=0.004."
|
||||
"Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8, "
|
||||
"is_short=False, leverage=1, open_rate=0.245441, open_since=closed), fee=0.004."
|
||||
),
|
||||
),
|
||||
# invalid, no currency in from fee dict
|
||||
@@ -3929,8 +3941,8 @@ def test_get_real_amount_multi(
|
||||
assert freqtrade.get_real_amount(trade, buy_order_fee, order_obj) == expected_amount
|
||||
assert log_has(
|
||||
(
|
||||
"Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, "
|
||||
"is_short=False, leverage=1.0, open_rate=0.24544100, open_since=closed), "
|
||||
"Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8, "
|
||||
"is_short=False, leverage=1, open_rate=0.245441, open_since=closed), "
|
||||
f"fee={expected_amount}."
|
||||
),
|
||||
caplog,
|
||||
@@ -4513,6 +4525,7 @@ def test_check_for_open_trades(mocker, default_conf_usdt, fee, is_short):
|
||||
def test_startup_update_open_orders(mocker, default_conf_usdt, fee, caplog, is_short):
|
||||
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||
create_mock_trades(fee, is_short=is_short)
|
||||
mocker.patch(f"{EXMS}._dry_is_price_crossed", return_value=False)
|
||||
|
||||
freqtrade.startup_update_open_orders()
|
||||
assert not log_has_re(r"Error updating Order .*", caplog)
|
||||
|
||||
@@ -50,16 +50,20 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee,
|
||||
stoploss_order_mock = MagicMock(side_effect=stop_orders)
|
||||
# Sell 3rd trade (not called for the first trade)
|
||||
should_sell_mock = MagicMock(side_effect=[[], [ExitCheckTuple(exit_type=ExitType.EXIT_SIGNAL)]])
|
||||
cancel_order_mock = MagicMock()
|
||||
|
||||
def patch_stoploss(order_id, *args, **kwargs):
|
||||
slo = stoploss_order_open.copy()
|
||||
slo["id"] = order_id
|
||||
slo["status"] = "canceled"
|
||||
return slo
|
||||
|
||||
cancel_order_mock = MagicMock(side_effect=patch_stoploss)
|
||||
mocker.patch.multiple(
|
||||
EXMS,
|
||||
create_stoploss=stoploss,
|
||||
fetch_ticker=ticker,
|
||||
get_fee=fee,
|
||||
amount_to_precision=lambda s, x, y: y,
|
||||
price_to_precision=lambda s, x, y: y,
|
||||
fetch_stoploss_order=stoploss_order_mock,
|
||||
cancel_stoploss_order_with_result=cancel_order_mock,
|
||||
)
|
||||
|
||||
mocker.patch.multiple(
|
||||
@@ -73,6 +77,12 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee,
|
||||
mocker.patch("freqtrade.wallets.Wallets.check_exit_amount", return_value=True)
|
||||
|
||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||
mocker.patch.multiple(
|
||||
freqtrade.exchange,
|
||||
create_stoploss=stoploss,
|
||||
fetch_stoploss_order=stoploss_order_mock,
|
||||
cancel_stoploss_order_with_result=cancel_order_mock,
|
||||
)
|
||||
freqtrade.strategy.order_types["stoploss_on_exchange"] = True
|
||||
# Switch ordertype to market to close trade immediately
|
||||
freqtrade.strategy.order_types["exit"] = "market"
|
||||
@@ -793,9 +803,13 @@ def test_dca_handle_similar_open_order(
|
||||
# Should Create a new exit order
|
||||
freqtrade.exchange.amount_to_contract_precision = MagicMock(return_value=2)
|
||||
freqtrade.strategy.adjust_trade_position = MagicMock(return_value=-2)
|
||||
msg = r"Skipping cancelling stoploss on exchange for.*"
|
||||
|
||||
mocker.patch(f"{EXMS}._dry_is_price_crossed", return_value=False)
|
||||
assert not log_has_re(msg, caplog)
|
||||
freqtrade.process()
|
||||
assert log_has_re(msg, caplog)
|
||||
|
||||
trade = Trade.get_trades().first()
|
||||
|
||||
assert trade.orders[-2].status == "closed"
|
||||
|
||||
@@ -103,7 +103,7 @@ def test_handle_stoploss_on_exchange(
|
||||
trade.is_open = True
|
||||
|
||||
hanging_stoploss_order = MagicMock(return_value={"id": "13434334", "status": "open"})
|
||||
mocker.patch(f"{EXMS}.fetch_stoploss_order", hanging_stoploss_order)
|
||||
mocker.patch.object(freqtrade.exchange, "fetch_stoploss_order", hanging_stoploss_order)
|
||||
|
||||
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
||||
hanging_stoploss_order.assert_called_once_with("13434334", trade.pair)
|
||||
@@ -116,7 +116,7 @@ def test_handle_stoploss_on_exchange(
|
||||
trade.is_open = True
|
||||
|
||||
canceled_stoploss_order = MagicMock(return_value={"id": "13434334", "status": "canceled"})
|
||||
mocker.patch(f"{EXMS}.fetch_stoploss_order", canceled_stoploss_order)
|
||||
mocker.patch.object(freqtrade.exchange, "fetch_stoploss_order", canceled_stoploss_order)
|
||||
stoploss.reset_mock()
|
||||
amount_before = trade.amount
|
||||
|
||||
@@ -149,7 +149,7 @@ def test_handle_stoploss_on_exchange(
|
||||
"amount": enter_order["amount"],
|
||||
}
|
||||
)
|
||||
mocker.patch(f"{EXMS}.fetch_stoploss_order", stoploss_order_hit)
|
||||
mocker.patch.object(freqtrade.exchange, "fetch_stoploss_order", stoploss_order_hit)
|
||||
freqtrade.strategy.order_filled = MagicMock(return_value=None)
|
||||
assert freqtrade.handle_stoploss_on_exchange(trade) is True
|
||||
assert log_has_re(r"STOP_LOSS_LIMIT is hit for Trade\(id=1, .*\)\.", caplog)
|
||||
@@ -158,7 +158,7 @@ def test_handle_stoploss_on_exchange(
|
||||
assert freqtrade.strategy.order_filled.call_count == 1
|
||||
caplog.clear()
|
||||
|
||||
mocker.patch(f"{EXMS}.create_stoploss", side_effect=ExchangeError())
|
||||
mocker.patch.object(freqtrade.exchange, "create_stoploss", side_effect=ExchangeError())
|
||||
trade.is_open = True
|
||||
freqtrade.handle_stoploss_on_exchange(trade)
|
||||
assert log_has("Unable to place a stoploss order on exchange.", caplog)
|
||||
@@ -168,8 +168,13 @@ def test_handle_stoploss_on_exchange(
|
||||
# It should try to add stoploss order
|
||||
stop_order_dict.update({"id": "105"})
|
||||
stoploss.reset_mock()
|
||||
mocker.patch(f"{EXMS}.fetch_stoploss_order", side_effect=InvalidOrderException())
|
||||
mocker.patch(f"{EXMS}.create_stoploss", stoploss)
|
||||
mocker.patch.multiple(
|
||||
freqtrade.exchange,
|
||||
fetch_stoploss_order=MagicMock(
|
||||
side_effect=InvalidOrderException(),
|
||||
),
|
||||
create_stoploss=stoploss,
|
||||
)
|
||||
freqtrade.handle_stoploss_on_exchange(trade)
|
||||
assert len(trade.open_sl_orders) == 1
|
||||
assert stoploss.call_count == 1
|
||||
@@ -179,8 +184,7 @@ def test_handle_stoploss_on_exchange(
|
||||
trade.is_open = False
|
||||
trade.open_sl_orders[-1].ft_is_open = False
|
||||
stoploss.reset_mock()
|
||||
mocker.patch(f"{EXMS}.fetch_order")
|
||||
mocker.patch(f"{EXMS}.create_stoploss", stoploss)
|
||||
mocker.patch.multiple(freqtrade.exchange, fetch_order=MagicMock(), create_stoploss=stoploss)
|
||||
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
||||
assert trade.has_open_sl_orders is False
|
||||
assert stoploss.call_count == 0
|
||||
@@ -252,9 +256,12 @@ def test_handle_stoploss_on_exchange_emergency(
|
||||
stoploss = MagicMock(side_effect=InvalidOrderException())
|
||||
assert trade.has_open_sl_orders is True
|
||||
Trade.commit()
|
||||
mocker.patch(f"{EXMS}.cancel_stoploss_order_with_result", side_effect=InvalidOrderException())
|
||||
mocker.patch(f"{EXMS}.fetch_stoploss_order", stoploss_order_cancelled)
|
||||
mocker.patch(f"{EXMS}.create_stoploss", stoploss)
|
||||
mocker.patch.multiple(
|
||||
freqtrade.exchange,
|
||||
cancel_stoploss_order_with_result=MagicMock(side_effect=InvalidOrderException()),
|
||||
fetch_stoploss_order=stoploss_order_cancelled,
|
||||
create_stoploss=stoploss,
|
||||
)
|
||||
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
||||
assert trade.has_open_sl_orders is False
|
||||
assert trade.is_open is False
|
||||
@@ -311,7 +318,7 @@ def test_handle_stoploss_on_exchange_partial(
|
||||
"amount": enter_order["amount"],
|
||||
}
|
||||
)
|
||||
mocker.patch(f"{EXMS}.fetch_stoploss_order", stoploss_order_hit)
|
||||
mocker.patch.multiple(freqtrade.exchange, fetch_stoploss_order=stoploss_order_hit)
|
||||
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
||||
# Stoploss filled partially ...
|
||||
assert trade.amount == 15
|
||||
@@ -383,8 +390,11 @@ def test_handle_stoploss_on_exchange_partial_cancel_here(
|
||||
"amount": enter_order["amount"],
|
||||
}
|
||||
)
|
||||
mocker.patch(f"{EXMS}.fetch_stoploss_order", stoploss_order_hit)
|
||||
mocker.patch(f"{EXMS}.cancel_stoploss_order_with_result", stoploss_order_cancel)
|
||||
mocker.patch.multiple(
|
||||
freqtrade.exchange,
|
||||
fetch_stoploss_order=stoploss_order_hit,
|
||||
cancel_stoploss_order_with_result=stoploss_order_cancel,
|
||||
)
|
||||
time_machine.shift(timedelta(minutes=15))
|
||||
|
||||
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
||||
@@ -408,20 +418,20 @@ def test_handle_sle_cancel_cant_recreate(
|
||||
mocker.patch.multiple(
|
||||
EXMS,
|
||||
fetch_ticker=MagicMock(return_value={"bid": 1.9, "ask": 2.2, "last": 1.9}),
|
||||
get_fee=fee,
|
||||
)
|
||||
freqtrade = FreqtradeBot(default_conf_usdt)
|
||||
mocker.patch.multiple(
|
||||
freqtrade.exchange,
|
||||
create_order=MagicMock(
|
||||
side_effect=[
|
||||
enter_order,
|
||||
exit_order,
|
||||
]
|
||||
),
|
||||
get_fee=fee,
|
||||
)
|
||||
mocker.patch.multiple(
|
||||
EXMS,
|
||||
fetch_stoploss_order=MagicMock(return_value={"status": "canceled", "id": "100"}),
|
||||
create_stoploss=MagicMock(side_effect=ExchangeError()),
|
||||
)
|
||||
freqtrade = FreqtradeBot(default_conf_usdt)
|
||||
patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
|
||||
|
||||
freqtrade.enter_positions()
|
||||
@@ -644,8 +654,11 @@ def test_handle_stoploss_on_exchange_trailing(
|
||||
stoploss_order_cancel = deepcopy(stoploss_order_hanging)
|
||||
stoploss_order_cancel["status"] = "canceled"
|
||||
|
||||
mocker.patch(f"{EXMS}.fetch_stoploss_order", return_value=stoploss_order_hanging)
|
||||
mocker.patch(f"{EXMS}.cancel_stoploss_order", return_value=stoploss_order_cancel)
|
||||
mocker.patch.multiple(
|
||||
freqtrade.exchange,
|
||||
fetch_stoploss_order=MagicMock(return_value=stoploss_order_hanging),
|
||||
cancel_stoploss_order=MagicMock(return_value=stoploss_order_cancel),
|
||||
)
|
||||
|
||||
# stoploss initially at 5%
|
||||
assert freqtrade.handle_trade(trade) is False
|
||||
@@ -671,9 +684,12 @@ def test_handle_stoploss_on_exchange_trailing(
|
||||
return_value={"id": "13434334", "status": "canceled", "fee": {}, "amount": trade.amount}
|
||||
)
|
||||
stoploss_order_mock = MagicMock(return_value={"id": "so1", "status": "open"})
|
||||
mocker.patch(f"{EXMS}.fetch_stoploss_order")
|
||||
mocker.patch(f"{EXMS}.cancel_stoploss_order", cancel_order_mock)
|
||||
mocker.patch(f"{EXMS}.create_stoploss", stoploss_order_mock)
|
||||
mocker.patch.multiple(
|
||||
freqtrade.exchange,
|
||||
fetch_stoploss_order=MagicMock(),
|
||||
cancel_stoploss_order=cancel_order_mock,
|
||||
create_stoploss=stoploss_order_mock,
|
||||
)
|
||||
|
||||
# stoploss should not be updated as the interval is 60 seconds
|
||||
assert freqtrade.handle_trade(trade) is False
|
||||
@@ -711,8 +727,9 @@ def test_handle_stoploss_on_exchange_trailing(
|
||||
}
|
||||
),
|
||||
)
|
||||
mocker.patch(
|
||||
f"{EXMS}.cancel_stoploss_order_with_result",
|
||||
mocker.patch.object(
|
||||
freqtrade.exchange,
|
||||
"cancel_stoploss_order_with_result",
|
||||
return_value={"id": "so1", "status": "canceled"},
|
||||
)
|
||||
assert len(trade.open_sl_orders) == 1
|
||||
@@ -786,8 +803,12 @@ def test_handle_stoploss_on_exchange_trailing_error(
|
||||
order_date=dt_now(),
|
||||
)
|
||||
)
|
||||
mocker.patch(f"{EXMS}.cancel_stoploss_order", side_effect=InvalidOrderException())
|
||||
mocker.patch(f"{EXMS}.fetch_stoploss_order", return_value=stoploss_order_hanging)
|
||||
mocker.patch.object(
|
||||
freqtrade.exchange, "cancel_stoploss_order", side_effect=InvalidOrderException()
|
||||
)
|
||||
mocker.patch.object(
|
||||
freqtrade.exchange, "fetch_stoploss_order", return_value=stoploss_order_hanging
|
||||
)
|
||||
time_machine.shift(timedelta(minutes=50))
|
||||
freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging)
|
||||
assert log_has_re(r"Could not cancel stoploss order abcd for pair ETH/USDT.*", caplog)
|
||||
@@ -799,8 +820,8 @@ def test_handle_stoploss_on_exchange_trailing_error(
|
||||
|
||||
# Fail creating stoploss order
|
||||
caplog.clear()
|
||||
cancel_mock = mocker.patch(f"{EXMS}.cancel_stoploss_order")
|
||||
mocker.patch(f"{EXMS}.create_stoploss", side_effect=ExchangeError())
|
||||
cancel_mock = mocker.patch.object(freqtrade.exchange, "cancel_stoploss_order")
|
||||
mocker.patch.object(freqtrade.exchange, "create_stoploss", side_effect=ExchangeError())
|
||||
time_machine.shift(timedelta(minutes=50))
|
||||
freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging)
|
||||
assert cancel_mock.call_count == 2
|
||||
@@ -846,20 +867,9 @@ def test_handle_stoploss_on_exchange_custom_stop(
|
||||
mocker.patch.multiple(
|
||||
EXMS,
|
||||
fetch_ticker=MagicMock(return_value={"bid": 1.9, "ask": 2.2, "last": 1.9}),
|
||||
create_order=MagicMock(
|
||||
side_effect=[
|
||||
enter_order,
|
||||
exit_order,
|
||||
]
|
||||
),
|
||||
get_fee=fee,
|
||||
is_cancel_order_result_suitable=MagicMock(return_value=True),
|
||||
)
|
||||
mocker.patch.multiple(
|
||||
EXMS,
|
||||
create_stoploss=stoploss,
|
||||
stoploss_adjust=MagicMock(return_value=True),
|
||||
)
|
||||
|
||||
# enabling TSL
|
||||
default_conf_usdt["use_custom_stoploss"] = True
|
||||
@@ -868,6 +878,17 @@ def test_handle_stoploss_on_exchange_custom_stop(
|
||||
default_conf_usdt["minimal_roi"]["0"] = 999999999
|
||||
|
||||
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||
mocker.patch.multiple(
|
||||
freqtrade.exchange,
|
||||
create_order=MagicMock(
|
||||
side_effect=[
|
||||
enter_order,
|
||||
exit_order,
|
||||
]
|
||||
),
|
||||
create_stoploss=stoploss,
|
||||
stoploss_adjust=MagicMock(return_value=True),
|
||||
)
|
||||
|
||||
# enabling stoploss on exchange
|
||||
freqtrade.strategy.order_types["stoploss_on_exchange"] = True
|
||||
@@ -912,8 +933,11 @@ def test_handle_stoploss_on_exchange_custom_stop(
|
||||
x["id"] = order_id
|
||||
return x
|
||||
|
||||
mocker.patch(f"{EXMS}.fetch_stoploss_order", MagicMock(fetch_stoploss_order_mock))
|
||||
mocker.patch(f"{EXMS}.cancel_stoploss_order", return_value=slo_canceled)
|
||||
mocker.patch.multiple(
|
||||
freqtrade.exchange,
|
||||
fetch_stoploss_order=MagicMock(fetch_stoploss_order_mock),
|
||||
cancel_stoploss_order=MagicMock(return_value=slo_canceled),
|
||||
)
|
||||
|
||||
assert freqtrade.handle_trade(trade) is False
|
||||
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
||||
@@ -932,8 +956,11 @@ def test_handle_stoploss_on_exchange_custom_stop(
|
||||
|
||||
cancel_order_mock = MagicMock()
|
||||
stoploss_order_mock = MagicMock(return_value={"id": "so1", "status": "open"})
|
||||
mocker.patch(f"{EXMS}.cancel_stoploss_order", cancel_order_mock)
|
||||
mocker.patch(f"{EXMS}.create_stoploss", stoploss_order_mock)
|
||||
mocker.patch.multiple(
|
||||
freqtrade.exchange,
|
||||
cancel_stoploss_order=cancel_order_mock,
|
||||
create_stoploss=stoploss_order_mock,
|
||||
)
|
||||
|
||||
# stoploss should not be updated as the interval is 60 seconds
|
||||
assert freqtrade.handle_trade(trade) is False
|
||||
@@ -1054,7 +1081,9 @@ def test_execute_trade_exit_sloe_cancel_exception(
|
||||
mocker, default_conf_usdt, ticker_usdt, fee, caplog
|
||||
) -> None:
|
||||
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||
mocker.patch(f"{EXMS}.cancel_stoploss_order", side_effect=InvalidOrderException())
|
||||
mocker.patch.object(
|
||||
freqtrade.exchange, "cancel_stoploss_order", side_effect=InvalidOrderException()
|
||||
)
|
||||
mocker.patch("freqtrade.wallets.Wallets.get_free", MagicMock(return_value=300))
|
||||
create_order_mock = MagicMock(
|
||||
side_effect=[
|
||||
@@ -1114,12 +1143,15 @@ def test_execute_trade_exit_with_stoploss_on_exchange(
|
||||
get_fee=fee,
|
||||
amount_to_precision=lambda s, x, y: y,
|
||||
price_to_precision=lambda s, x, y: y,
|
||||
)
|
||||
freqtrade = FreqtradeBot(default_conf_usdt)
|
||||
mocker.patch.multiple(
|
||||
freqtrade.exchange,
|
||||
create_stoploss=stoploss,
|
||||
cancel_stoploss_order=cancel_order,
|
||||
_dry_is_price_crossed=MagicMock(side_effect=[True, False]),
|
||||
)
|
||||
|
||||
freqtrade = FreqtradeBot(default_conf_usdt)
|
||||
freqtrade.strategy.order_types["stoploss_on_exchange"] = True
|
||||
patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
|
||||
|
||||
@@ -1208,7 +1240,7 @@ def test_may_execute_trade_exit_after_stoploss_on_exchange_hit(
|
||||
"trades": None,
|
||||
}
|
||||
)
|
||||
mocker.patch(f"{EXMS}.fetch_stoploss_order", stoploss_executed)
|
||||
mocker.patch.object(freqtrade.exchange, "fetch_stoploss_order", stoploss_executed)
|
||||
|
||||
freqtrade.exit_positions(trades)
|
||||
assert trade.has_open_sl_orders is False
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -879,6 +879,10 @@ def test_backtest_one_detail(default_conf_usdt, mocker, testdatadir, use_detail)
|
||||
patch_exchange(mocker)
|
||||
mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001)
|
||||
mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float("inf"))
|
||||
default_conf_usdt["unfilledtimeout"] = {
|
||||
"entry": 11,
|
||||
"exit": 30,
|
||||
}
|
||||
if use_detail:
|
||||
default_conf_usdt["timeframe_detail"] = "1m"
|
||||
|
||||
@@ -916,7 +920,7 @@ def test_backtest_one_detail(default_conf_usdt, mocker, testdatadir, use_detail)
|
||||
)
|
||||
results = result["results"]
|
||||
assert not results.empty
|
||||
# Timeout settings from default_conf = entry: 10, exit: 30
|
||||
# Timeout settings from = entry: 11, exit: 30
|
||||
assert len(results) == (2 if use_detail else 3)
|
||||
|
||||
assert "orders" in results.columns
|
||||
@@ -966,8 +970,8 @@ def test_backtest_one_detail(default_conf_usdt, mocker, testdatadir, use_detail)
|
||||
@pytest.mark.parametrize(
|
||||
"use_detail,exp_funding_fee, exp_ff_updates",
|
||||
[
|
||||
(True, -0.018054162, 10),
|
||||
(False, -0.01780296, 6),
|
||||
(True, -0.0180457882, 15),
|
||||
(False, -0.0178000543, 12),
|
||||
],
|
||||
)
|
||||
def test_backtest_one_detail_futures(
|
||||
@@ -1077,8 +1081,8 @@ def test_backtest_one_detail_futures(
|
||||
@pytest.mark.parametrize(
|
||||
"use_detail,entries,max_stake,ff_updates,expected_ff",
|
||||
[
|
||||
(True, 50, 3000, 55, -1.18038144),
|
||||
(False, 6, 360, 11, -0.14679994),
|
||||
(True, 50, 3000, 78, -1.17988972),
|
||||
(False, 6, 360, 34, -0.14673681),
|
||||
],
|
||||
)
|
||||
def test_backtest_one_detail_futures_funding_fees(
|
||||
@@ -1800,7 +1804,7 @@ def test_backtest_multi_pair_detail_simplified(
|
||||
if use_detail:
|
||||
# Backtest loop is called once per candle per pair
|
||||
# Exact numbers depend on trade state - but should be around 2_600
|
||||
assert bl_spy.call_count > 2_170
|
||||
assert bl_spy.call_count > 2_159
|
||||
assert bl_spy.call_count < 2_800
|
||||
assert len(evaluate_result_multi(results["results"], "1h", 3)) > 0
|
||||
else:
|
||||
@@ -2378,13 +2382,12 @@ def test_backtest_start_nomock_futures(default_conf_usdt, mocker, caplog, testda
|
||||
f"Using data directory: {testdatadir} ...",
|
||||
"Loading data from 2021-11-17 01:00:00 up to 2021-11-21 04:00:00 (4 days).",
|
||||
"Backtesting with data from 2021-11-17 21:00:00 up to 2021-11-21 04:00:00 (3 days).",
|
||||
"XRP/USDT:USDT, funding_rate, 8h, data starts at 2021-11-18 00:00:00",
|
||||
"XRP/USDT:USDT, mark, 8h, data starts at 2021-11-18 00:00:00",
|
||||
"XRP/USDT:USDT, funding_rate, 1h, data starts at 2021-11-18 00:00:00",
|
||||
f"Running backtesting for Strategy {CURRENT_TEST_STRATEGY}",
|
||||
]
|
||||
|
||||
for line in exists:
|
||||
assert log_has(line, caplog)
|
||||
assert log_has(line, caplog), line
|
||||
|
||||
captured = capsys.readouterr()
|
||||
assert "BACKTESTING REPORT" in captured.out
|
||||
@@ -2772,7 +2775,7 @@ def test_time_pair_generator_open_trades_first(mocker, default_conf, dynamic_pai
|
||||
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):
|
||||
def mock_refresh(self, **kwargs):
|
||||
# Simulate shuffle
|
||||
self._whitelist = pairs[::-1] # ['ETH/BTC', 'NEO/BTC', 'LTC/BTC', 'XRP/BTC']
|
||||
|
||||
|
||||
@@ -236,28 +236,6 @@ def test_start_not_installed(mocker, default_conf, import_fails) -> None:
|
||||
start_hyperopt(pargs)
|
||||
|
||||
|
||||
def test_start_no_hyperopt_allowed(mocker, hyperopt_conf, caplog) -> None:
|
||||
start_mock = MagicMock()
|
||||
patched_configuration_load_config_file(mocker, hyperopt_conf)
|
||||
mocker.patch("freqtrade.optimize.hyperopt.Hyperopt.start", start_mock)
|
||||
patch_exchange(mocker)
|
||||
|
||||
args = [
|
||||
"hyperopt",
|
||||
"--config",
|
||||
"config.json",
|
||||
"--hyperopt",
|
||||
"HyperoptTestSepFile",
|
||||
"--hyperopt-loss",
|
||||
"SharpeHyperOptLossDaily",
|
||||
"--epochs",
|
||||
"5",
|
||||
]
|
||||
pargs = get_args(args)
|
||||
with pytest.raises(OperationalException, match=r"Using separate Hyperopt files has been.*"):
|
||||
start_hyperopt(pargs)
|
||||
|
||||
|
||||
def test_start_no_data(mocker, hyperopt_conf, tmp_path) -> None:
|
||||
hyperopt_conf["user_data_dir"] = tmp_path
|
||||
patched_configuration_load_config_file(mocker, hyperopt_conf)
|
||||
@@ -523,7 +501,7 @@ def test_populate_indicators(hyperopt, testdatadir) -> None:
|
||||
def test_generate_optimizer(mocker, hyperopt_conf) -> None:
|
||||
hyperopt_conf.update(
|
||||
{
|
||||
"spaces": "all",
|
||||
"spaces": ["all"],
|
||||
"hyperopt_min_trades": 1,
|
||||
}
|
||||
)
|
||||
@@ -591,6 +569,8 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None:
|
||||
"buy_rsi": 35,
|
||||
"sell_minusdi": 0.02,
|
||||
"sell_rsi": 75,
|
||||
"exit_rsi": 7,
|
||||
"exitaaa": 7,
|
||||
"protection_cooldown_lookback": 20,
|
||||
"protection_enabled": True,
|
||||
"roi_t1": 60.0,
|
||||
@@ -619,6 +599,12 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None:
|
||||
"buy_plusdi": 0.02,
|
||||
"buy_rsi": 35,
|
||||
},
|
||||
"exitaspace": {
|
||||
"exitaaa": 7,
|
||||
},
|
||||
"exit": {
|
||||
"exit_rsi": 7,
|
||||
},
|
||||
"roi": {"0": 0.12, "20.0": 0.02, "50.0": 0.01, "110.0": 0},
|
||||
"protection": {
|
||||
"protection_cooldown_lookback": 20,
|
||||
@@ -638,7 +624,7 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None:
|
||||
"max_open_trades": {"max_open_trades": 3},
|
||||
},
|
||||
"params_dict": optimizer_param,
|
||||
"params_not_optimized": {"buy": {}, "protection": {}, "sell": {}},
|
||||
"params_not_optimized": {},
|
||||
"results_metrics": ANY,
|
||||
"total_profit": 3.1e-08,
|
||||
}
|
||||
@@ -708,7 +694,7 @@ def test_print_json_spaces_all(mocker, hyperopt_conf, capsys) -> None:
|
||||
|
||||
hyperopt_conf.update(
|
||||
{
|
||||
"spaces": "all",
|
||||
"spaces": ["all"],
|
||||
"hyperopt_jobs": 1,
|
||||
"print_json": True,
|
||||
}
|
||||
@@ -824,7 +810,7 @@ def test_print_json_spaces_roi_stoploss(mocker, hyperopt_conf, capsys) -> None:
|
||||
|
||||
hyperopt_conf.update(
|
||||
{
|
||||
"spaces": "roi stoploss",
|
||||
"spaces": ["roi", "stoploss"],
|
||||
"hyperopt_jobs": 1,
|
||||
"print_json": True,
|
||||
}
|
||||
@@ -876,7 +862,7 @@ def test_simplified_interface_roi_stoploss(mocker, hyperopt_conf, capsys) -> Non
|
||||
)
|
||||
patch_exchange(mocker)
|
||||
|
||||
hyperopt_conf.update({"spaces": "roi stoploss"})
|
||||
hyperopt_conf.update({"spaces": ["roi", "stoploss"]})
|
||||
|
||||
hyperopt = Hyperopt(hyperopt_conf)
|
||||
hyperopt.hyperopter.backtesting.strategy.advise_all_indicators = MagicMock()
|
||||
@@ -915,27 +901,52 @@ def test_simplified_interface_all_failed(mocker, hyperopt_conf, caplog) -> None:
|
||||
|
||||
hyperopt_conf.update(
|
||||
{
|
||||
"spaces": "all",
|
||||
"spaces": ["all"],
|
||||
}
|
||||
)
|
||||
|
||||
mocker.patch(
|
||||
"freqtrade.optimize.hyperopt.hyperopt_auto.HyperOptAuto._generate_indicator_space",
|
||||
return_value=[],
|
||||
)
|
||||
|
||||
hyperopt = Hyperopt(hyperopt_conf)
|
||||
hyperopt.hyperopter.backtesting.strategy.advise_all_indicators = MagicMock()
|
||||
hyperopt.hyperopter.backtesting.strategy.enumerate_parameters = MagicMock(return_value=[])
|
||||
hyperopt.hyperopter.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
|
||||
|
||||
with pytest.raises(OperationalException, match=r"The 'protection' space is included into *"):
|
||||
# The first one to fail raises the exception
|
||||
with pytest.raises(OperationalException, match=r"The 'buy' space is included into *"):
|
||||
hyperopt.hyperopter.init_spaces()
|
||||
|
||||
hyperopt.config["hyperopt_ignore_missing_space"] = True
|
||||
caplog.clear()
|
||||
hyperopt.hyperopter.init_spaces()
|
||||
assert log_has_re(r"The 'protection' space is included into *", caplog)
|
||||
assert hyperopt.hyperopter.protection_space == []
|
||||
assert hyperopt.hyperopter.spaces["protection"] == []
|
||||
|
||||
|
||||
def test_simplified_interface_none_selected(mocker, hyperopt_conf, caplog) -> None:
|
||||
mocker.patch("freqtrade.optimize.hyperopt.hyperopt_optimizer.dump", MagicMock())
|
||||
mocker.patch("freqtrade.optimize.hyperopt.hyperopt.file_dump_json")
|
||||
mocker.patch(
|
||||
"freqtrade.optimize.backtesting.Backtesting.load_bt_data",
|
||||
MagicMock(return_value=(MagicMock(), None)),
|
||||
)
|
||||
mocker.patch(
|
||||
"freqtrade.optimize.hyperopt.hyperopt_optimizer.get_timerange",
|
||||
MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))),
|
||||
)
|
||||
|
||||
patch_exchange(mocker)
|
||||
|
||||
hyperopt_conf.update(
|
||||
{
|
||||
"spaces": [],
|
||||
}
|
||||
)
|
||||
|
||||
hyperopt = Hyperopt(hyperopt_conf)
|
||||
hyperopt.hyperopter.backtesting.strategy.advise_all_indicators = MagicMock()
|
||||
hyperopt.hyperopter.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
|
||||
|
||||
with pytest.raises(OperationalException, match=r"No hyperopt parameters found to optimize\..*"):
|
||||
hyperopt.hyperopter.init_spaces()
|
||||
|
||||
|
||||
def test_simplified_interface_buy(mocker, hyperopt_conf, capsys) -> None:
|
||||
@@ -969,7 +980,7 @@ def test_simplified_interface_buy(mocker, hyperopt_conf, capsys) -> None:
|
||||
)
|
||||
patch_exchange(mocker)
|
||||
|
||||
hyperopt_conf.update({"spaces": "buy"})
|
||||
hyperopt_conf.update({"spaces": ["buy"]})
|
||||
|
||||
hyperopt = Hyperopt(hyperopt_conf)
|
||||
hyperopt.hyperopter.backtesting.strategy.advise_all_indicators = MagicMock()
|
||||
@@ -1025,7 +1036,7 @@ def test_simplified_interface_sell(mocker, hyperopt_conf, capsys) -> None:
|
||||
|
||||
hyperopt_conf.update(
|
||||
{
|
||||
"spaces": "sell",
|
||||
"spaces": ["sell"],
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1069,16 +1080,13 @@ def test_simplified_interface_failed(mocker, hyperopt_conf, space) -> None:
|
||||
"freqtrade.optimize.hyperopt.hyperopt_optimizer.get_timerange",
|
||||
MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))),
|
||||
)
|
||||
mocker.patch(
|
||||
"freqtrade.optimize.hyperopt.hyperopt_auto.HyperOptAuto._generate_indicator_space",
|
||||
return_value=[],
|
||||
)
|
||||
|
||||
patch_exchange(mocker)
|
||||
|
||||
hyperopt_conf.update({"spaces": space})
|
||||
hyperopt_conf.update({"spaces": [space]})
|
||||
|
||||
hyperopt = Hyperopt(hyperopt_conf)
|
||||
hyperopt.hyperopter.backtesting.strategy.enumerate_parameters = MagicMock(return_value=[])
|
||||
hyperopt.hyperopter.backtesting.strategy.advise_all_indicators = MagicMock()
|
||||
hyperopt.hyperopter.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
|
||||
|
||||
@@ -1132,7 +1140,9 @@ def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmp_path, fee) -> None
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings("ignore::DeprecationWarning")
|
||||
def test_in_strategy_auto_hyperopt_with_parallel(mocker, hyperopt_conf, tmp_path, fee) -> None:
|
||||
def test_in_strategy_auto_hyperopt_with_parallel(
|
||||
mocker, hyperopt_conf, tmp_path, fee, caplog
|
||||
) -> None:
|
||||
mocker.patch(f"{EXMS}.validate_config", MagicMock())
|
||||
mocker.patch(f"{EXMS}.get_fee", fee)
|
||||
mocker.patch(f"{EXMS}.reload_markets")
|
||||
@@ -1175,6 +1185,8 @@ def test_in_strategy_auto_hyperopt_with_parallel(mocker, hyperopt_conf, tmp_path
|
||||
assert len(list(buy_rsi_range)) == 51
|
||||
|
||||
hyperopt.start()
|
||||
# Test logs from parallel workers are shown.
|
||||
assert log_has("Test: Bot loop started", caplog)
|
||||
|
||||
|
||||
def test_in_strategy_auto_hyperopt_per_epoch(mocker, hyperopt_conf, tmp_path, fee) -> None:
|
||||
|
||||
@@ -296,14 +296,14 @@ def test_show_epoch_details(capsys):
|
||||
|
||||
HyperoptTools.show_epoch_details(test_result, 5, False, no_header=True)
|
||||
captured = capsys.readouterr()
|
||||
assert "# Trailing stop:" in captured.out
|
||||
assert "# Trailing stop parameters:" in captured.out
|
||||
# re.match(r"Pairs for .*", captured.out)
|
||||
assert re.search(r"^\s+trailing_stop = True$", captured.out, re.MULTILINE)
|
||||
assert re.search(r"^\s+trailing_stop_positive = 0.02$", captured.out, re.MULTILINE)
|
||||
assert re.search(r"^\s+trailing_stop_positive_offset = 0.04$", captured.out, re.MULTILINE)
|
||||
assert re.search(r"^\s+trailing_only_offset_is_reached = True$", captured.out, re.MULTILINE)
|
||||
|
||||
assert "# ROI table:" in captured.out
|
||||
assert "# ROI parameters:" in captured.out
|
||||
assert re.search(r"^\s+minimal_roi = \{$", captured.out, re.MULTILINE)
|
||||
assert re.search(r"^\s+\"90\"\:\s0.14,\s*$", captured.out, re.MULTILINE)
|
||||
|
||||
|
||||
@@ -634,11 +634,30 @@ def test_generate_periodic_breakdown_stats(testdatadir):
|
||||
res = generate_periodic_breakdown_stats([], "day")
|
||||
assert res == []
|
||||
|
||||
# Test weekday
|
||||
reswd = generate_periodic_breakdown_stats(bt_data, "weekday")
|
||||
assert isinstance(reswd, list)
|
||||
assert len(reswd) == 7
|
||||
assert reswd[0]["date"] == "Monday"
|
||||
assert reswd[0]["date_ts"] == 0
|
||||
assert reswd[1]["date"] == "Tuesday"
|
||||
assert reswd[2]["date"] == "Wednesday"
|
||||
assert reswd[3]["date"] == "Thursday"
|
||||
assert reswd[4]["date"] == "Friday"
|
||||
assert reswd[5]["date"] == "Saturday"
|
||||
assert reswd[6]["date"] == "Sunday"
|
||||
monday = reswd[0]
|
||||
assert "draws" in monday
|
||||
assert "losses" in monday
|
||||
assert "wins" in monday
|
||||
assert "profit_abs" in monday
|
||||
|
||||
|
||||
def test__get_resample_from_period():
|
||||
assert _get_resample_from_period("day") == "1d"
|
||||
assert _get_resample_from_period("week") == "1W-MON"
|
||||
assert _get_resample_from_period("month") == "1ME"
|
||||
assert _get_resample_from_period("weekday") == "weekday"
|
||||
with pytest.raises(ValueError, match=r"Period noooo is not supported."):
|
||||
_get_resample_from_period("noooo")
|
||||
|
||||
|
||||
@@ -6,19 +6,19 @@ from freqtrade.persistence import FtNoDBContext, PairLocks, Trade
|
||||
@pytest.mark.parametrize("timeframe", ["", "5m", "1d"])
|
||||
def test_FtNoDBContext(timeframe):
|
||||
PairLocks.timeframe = ""
|
||||
assert Trade.use_db is True
|
||||
assert PairLocks.use_db is True
|
||||
assert Trade.use_db
|
||||
assert PairLocks.use_db
|
||||
assert PairLocks.timeframe == ""
|
||||
|
||||
with FtNoDBContext(timeframe):
|
||||
assert Trade.use_db is False
|
||||
assert PairLocks.use_db is False
|
||||
assert not Trade.use_db
|
||||
assert not PairLocks.use_db
|
||||
assert PairLocks.timeframe == timeframe
|
||||
|
||||
with FtNoDBContext():
|
||||
assert Trade.use_db is False
|
||||
assert PairLocks.use_db is False
|
||||
assert not Trade.use_db
|
||||
assert not PairLocks.use_db
|
||||
assert PairLocks.timeframe == ""
|
||||
|
||||
assert Trade.use_db is True
|
||||
assert PairLocks.use_db is True
|
||||
assert Trade.use_db
|
||||
assert PairLocks.use_db
|
||||
|
||||
@@ -441,7 +441,8 @@ def test_migrate_pairlocks(mocker, default_conf, fee, caplog):
|
||||
"dialect",
|
||||
[
|
||||
"sqlite",
|
||||
"postgresql",
|
||||
"postgresql", # test for psycopg2 compat
|
||||
"postgresql.psycopg", # test for psycopg3 compat
|
||||
"mysql",
|
||||
"oracle",
|
||||
"mssql",
|
||||
|
||||
@@ -372,8 +372,8 @@ def test_borrowed(fee, is_short, lev, borrowed, trading_mode):
|
||||
@pytest.mark.parametrize(
|
||||
"is_short,open_rate,close_rate,lev,profit,trading_mode",
|
||||
[
|
||||
(False, 2.0, 2.2, 1.0, 0.09451372, spot),
|
||||
(True, 2.2, 2.0, 3.0, 0.25894253, margin),
|
||||
(False, 2, 2.2, 1, 0.09451372, spot),
|
||||
(True, 2.2, 2.0, 3, 0.25894253, margin),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("init_persistence")
|
||||
@@ -493,8 +493,8 @@ def test_update_limit_order(
|
||||
assert trade.close_date is None
|
||||
assert log_has_re(
|
||||
f"LIMIT_{entry_side.upper()} has been fulfilled for "
|
||||
r"Trade\(id=2, pair=ADA/USDT, amount=30.00000000, "
|
||||
f"is_short={is_short}, leverage={lev}, open_rate={open_rate}0000000, "
|
||||
r"Trade\(id=2, pair=ADA/USDT, amount=30, "
|
||||
f"is_short={is_short}, leverage={lev}, open_rate={open_rate}, "
|
||||
r"open_since=.*\).",
|
||||
caplog,
|
||||
)
|
||||
@@ -511,8 +511,8 @@ def test_update_limit_order(
|
||||
assert trade.close_date is not None
|
||||
assert log_has_re(
|
||||
f"LIMIT_{exit_side.upper()} has been fulfilled for "
|
||||
r"Trade\(id=2, pair=ADA/USDT, amount=30.00000000, "
|
||||
f"is_short={is_short}, leverage={lev}, open_rate={open_rate}0000000, "
|
||||
r"Trade\(id=2, pair=ADA/USDT, amount=30, "
|
||||
f"is_short={is_short}, leverage={lev}, open_rate={open_rate}, "
|
||||
r"open_since=.*\).",
|
||||
caplog,
|
||||
)
|
||||
@@ -545,8 +545,8 @@ def test_update_market_order(market_buy_order_usdt, market_sell_order_usdt, fee,
|
||||
assert trade.close_date is None
|
||||
assert log_has_re(
|
||||
r"MARKET_BUY has been fulfilled for Trade\(id=1, "
|
||||
r"pair=ADA/USDT, amount=30.00000000, is_short=False, leverage=1.0, "
|
||||
r"open_rate=2.00000000, open_since=.*\).",
|
||||
r"pair=ADA/USDT, amount=30, is_short=False, leverage=1, "
|
||||
r"open_rate=2, open_since=.*\).",
|
||||
caplog,
|
||||
)
|
||||
|
||||
@@ -561,8 +561,8 @@ def test_update_market_order(market_buy_order_usdt, market_sell_order_usdt, fee,
|
||||
assert trade.close_date is not None
|
||||
assert log_has_re(
|
||||
r"MARKET_SELL has been fulfilled for Trade\(id=1, "
|
||||
r"pair=ADA/USDT, amount=30.00000000, is_short=False, leverage=1.0, "
|
||||
r"open_rate=2.00000000, open_since=.*\).",
|
||||
r"pair=ADA/USDT, amount=30, is_short=False, leverage=1, "
|
||||
r"open_rate=2, open_since=.*\).",
|
||||
caplog,
|
||||
)
|
||||
|
||||
@@ -1479,6 +1479,8 @@ def test_to_json(fee):
|
||||
"contract_size": 1,
|
||||
"orders": [],
|
||||
"has_open_orders": False,
|
||||
"nr_of_successful_entries": 0,
|
||||
"nr_of_successful_exits": 0,
|
||||
}
|
||||
|
||||
# Simulate dry_run entries
|
||||
@@ -1570,6 +1572,8 @@ def test_to_json(fee):
|
||||
"contract_size": 1,
|
||||
"orders": [],
|
||||
"has_open_orders": False,
|
||||
"nr_of_successful_entries": 0,
|
||||
"nr_of_successful_exits": 0,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1396,7 +1396,7 @@ def test_gen_pair_whitelist_not_supported(mocker, default_conf, tickers) -> None
|
||||
)
|
||||
|
||||
with pytest.raises(
|
||||
OperationalException, match=r"Exchange does not support dynamic whitelist.*"
|
||||
OperationalException, match=r"Exchange .* does not support dynamic whitelist.*"
|
||||
):
|
||||
get_patched_freqtradebot(mocker, default_conf)
|
||||
|
||||
@@ -1410,7 +1410,9 @@ def test_pair_whitelist_not_supported_Spread(mocker, default_conf, tickers) -> N
|
||||
exchange_has=MagicMock(return_value=False),
|
||||
)
|
||||
|
||||
with pytest.raises(OperationalException, match=r"Exchange does not support fetchTickers, .*"):
|
||||
with pytest.raises(
|
||||
OperationalException, match=r"Exchange .* does not support fetchTickers, .*"
|
||||
):
|
||||
get_patched_freqtradebot(mocker, default_conf)
|
||||
|
||||
mocker.patch(f"{EXMS}.exchange_has", MagicMock(return_value=True))
|
||||
@@ -2334,6 +2336,36 @@ def test_FullTradesFilter(mocker, default_conf_usdt, fee, caplog) -> None:
|
||||
["ETH/USDT:USDT", "ADA/USDT:USDT"],
|
||||
["layer-1", "protocol"],
|
||||
),
|
||||
(
|
||||
[
|
||||
# Blacklist high MC pairs
|
||||
{"method": "StaticPairList", "allow_inactive": True},
|
||||
{"method": "MarketCapPairList", "mode": "blacklist"},
|
||||
],
|
||||
"spot",
|
||||
["LTC/USDT", "NEO/USDT", "TKN/USDT", "ETC/USDT"],
|
||||
1,
|
||||
),
|
||||
(
|
||||
[
|
||||
# Blacklist high MC pairs
|
||||
{"method": "StaticPairList", "allow_inactive": True},
|
||||
{"method": "MarketCapPairList", "mode": "blacklist", "max_rank": 2},
|
||||
],
|
||||
"spot",
|
||||
["LTC/USDT", "XRP/USDT", "NEO/USDT", "TKN/USDT", "ETC/USDT", "ADA/USDT"],
|
||||
1,
|
||||
),
|
||||
(
|
||||
[
|
||||
# Blacklist top 6 MarketCap pairs - removes XRP which is at spot 6.
|
||||
{"method": "StaticPairList", "allow_inactive": True},
|
||||
{"method": "MarketCapPairList", "mode": "blacklist", "max_rank": 6},
|
||||
],
|
||||
"spot",
|
||||
["LTC/USDT", "NEO/USDT", "TKN/USDT", "ETC/USDT", "ADA/USDT"],
|
||||
1,
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_MarketCapPairList_filter(
|
||||
|
||||
@@ -43,9 +43,9 @@ def test_volume_change_pair_list_init_exchange_support(mocker, rpl_config):
|
||||
|
||||
with pytest.raises(
|
||||
OperationalException,
|
||||
match=r"Exchange does not support dynamic whitelist in this configuration. "
|
||||
match=r"Exchange .* does not support dynamic whitelist in this configuration. "
|
||||
r"Please edit your config and either remove PercentChangePairList, "
|
||||
r"or switch to using candles. and restart the bot.",
|
||||
r"or switch to using candles and restart the bot.",
|
||||
):
|
||||
get_patched_freqtradebot(mocker, rpl_config)
|
||||
|
||||
|
||||
@@ -12,6 +12,14 @@ from freqtrade.util.coin_gecko import FtCoinGeckoApi
|
||||
from tests.conftest import log_has, log_has_re
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset_singleton_instance():
|
||||
# Reset the singleton instance before each test
|
||||
CryptoToFiatConverter._instances = {}
|
||||
yield
|
||||
CryptoToFiatConverter._instances = {}
|
||||
|
||||
|
||||
def test_fiat_convert_is_singleton():
|
||||
fiat_convert = CryptoToFiatConverter({"a": 22})
|
||||
fiat_convert2 = CryptoToFiatConverter({})
|
||||
|
||||
+187
-50
@@ -17,6 +17,7 @@ from tests.conftest import (
|
||||
create_mock_trades,
|
||||
create_mock_trades_usdt,
|
||||
get_patched_freqtradebot,
|
||||
log_has_re,
|
||||
patch_get_signal,
|
||||
)
|
||||
|
||||
@@ -99,6 +100,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
||||
"contract_size": 1,
|
||||
"has_open_orders": False,
|
||||
"nr_of_successful_entries": ANY,
|
||||
"nr_of_successful_exits": ANY,
|
||||
"orders": [
|
||||
{
|
||||
"amount": 91.07468123,
|
||||
@@ -229,11 +231,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
||||
|
||||
def test_rpc_status_table(default_conf, ticker, fee, mocker, time_machine) -> None:
|
||||
time_machine.move_to("2024-05-10 11:15:00 +00:00", tick=False)
|
||||
mocker.patch.multiple(
|
||||
"freqtrade.rpc.fiat_convert.FtCoinGeckoApi",
|
||||
get_price=MagicMock(return_value={"bitcoin": {"usd": 15000.0}}),
|
||||
)
|
||||
mocker.patch("freqtrade.rpc.rpc.CryptoToFiatConverter._find_price", return_value=15000.0)
|
||||
|
||||
mocker.patch("freqtrade.rpc.telegram.Telegram", MagicMock())
|
||||
mocker.patch.multiple(
|
||||
EXMS,
|
||||
@@ -278,6 +276,8 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker, time_machine) -> No
|
||||
# Test with fiat convert
|
||||
rpc._config["fiat_display_currency"] = "USD"
|
||||
rpc._fiat_converter = CryptoToFiatConverter({})
|
||||
mocker.patch.object(rpc._fiat_converter, "get_price", return_value=15000.0)
|
||||
|
||||
result, headers, fiat_profit_sum, total_sum = rpc._rpc_status_table(
|
||||
default_conf["stake_currency"], "USD"
|
||||
)
|
||||
@@ -309,7 +309,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker, time_machine) -> No
|
||||
)
|
||||
assert "now" == result[0][2]
|
||||
assert "ETH/BTC" in result[0][1]
|
||||
assert "nan%" == result[0][3]
|
||||
assert "N/A" == result[0][3]
|
||||
assert isnan(fiat_profit_sum)
|
||||
|
||||
|
||||
@@ -385,11 +385,14 @@ def test_rpc_delete_trade(mocker, default_conf, fee, markets, caplog, is_short):
|
||||
mocker.patch.multiple(
|
||||
EXMS,
|
||||
markets=PropertyMock(return_value=markets),
|
||||
cancel_order=cancel_mock,
|
||||
cancel_stoploss_order=stoploss_mock,
|
||||
)
|
||||
|
||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||
mocker.patch.multiple(
|
||||
freqtradebot.exchange,
|
||||
cancel_order=cancel_mock,
|
||||
cancel_stoploss_order=stoploss_mock,
|
||||
)
|
||||
freqtradebot.strategy.order_types["stoploss_on_exchange"] = True
|
||||
create_mock_trades(fee, is_short)
|
||||
rpc = RPC(freqtradebot)
|
||||
@@ -425,13 +428,17 @@ def test_rpc_delete_trade(mocker, default_conf, fee, markets, caplog, is_short):
|
||||
assert stoploss_mock.call_count == 1
|
||||
assert res["cancel_order_count"] == 1
|
||||
|
||||
stoploss_mock = mocker.patch(f"{EXMS}.cancel_stoploss_order", side_effect=InvalidOrderException)
|
||||
stoploss_mock = mocker.patch.object(
|
||||
freqtradebot.exchange, "cancel_stoploss_order", side_effect=InvalidOrderException
|
||||
)
|
||||
|
||||
res = rpc._rpc_delete("3")
|
||||
assert stoploss_mock.call_count == 1
|
||||
stoploss_mock.reset_mock()
|
||||
|
||||
cancel_mock = mocker.patch(f"{EXMS}.cancel_order", side_effect=InvalidOrderException)
|
||||
cancel_mock = mocker.patch.object(
|
||||
freqtradebot.exchange, "cancel_order", side_effect=InvalidOrderException
|
||||
)
|
||||
|
||||
res = rpc._rpc_delete("4")
|
||||
assert cancel_mock.call_count == 1
|
||||
@@ -439,7 +446,6 @@ def test_rpc_delete_trade(mocker, default_conf, fee, markets, caplog, is_short):
|
||||
|
||||
|
||||
def test_rpc_trade_statistics(default_conf_usdt, ticker, fee, mocker) -> None:
|
||||
mocker.patch("freqtrade.rpc.rpc.CryptoToFiatConverter._find_price", return_value=1.1)
|
||||
mocker.patch("freqtrade.rpc.telegram.Telegram", MagicMock())
|
||||
mocker.patch.multiple(
|
||||
EXMS,
|
||||
@@ -453,6 +459,7 @@ def test_rpc_trade_statistics(default_conf_usdt, ticker, fee, mocker) -> None:
|
||||
|
||||
rpc = RPC(freqtradebot)
|
||||
rpc._fiat_converter = CryptoToFiatConverter({})
|
||||
mocker.patch.object(rpc._fiat_converter, "get_price", return_value=1.1)
|
||||
|
||||
res = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
|
||||
assert res["trade_count"] == 0
|
||||
@@ -497,7 +504,7 @@ def test_rpc_trade_statistics(default_conf_usdt, ticker, fee, mocker) -> None:
|
||||
assert isnan(stats["profit_all_coin"])
|
||||
|
||||
|
||||
def test_rpc_balance_handle_error(default_conf, mocker):
|
||||
def test_rpc_balance_handle_error(default_conf, mocker, caplog):
|
||||
mock_balance = {
|
||||
"BTC": {
|
||||
"free": 10.0,
|
||||
@@ -511,35 +518,73 @@ def test_rpc_balance_handle_error(default_conf, mocker):
|
||||
},
|
||||
}
|
||||
# ETH will be skipped due to mocked Error below
|
||||
mock_pos = [
|
||||
{
|
||||
"symbol": "ADA/USDT:USDT",
|
||||
"timestamp": None,
|
||||
"datetime": None,
|
||||
"initialMargin": 20,
|
||||
"initialMarginPercentage": None,
|
||||
"maintenanceMargin": 0.0,
|
||||
"maintenanceMarginPercentage": 0.005,
|
||||
"entryPrice": 0.0,
|
||||
"notional": 10.0,
|
||||
"leverage": 5.0,
|
||||
"unrealizedPnl": 0.0,
|
||||
"contracts": 1.0,
|
||||
"contractSize": 1,
|
||||
"marginRatio": None,
|
||||
"liquidationPrice": 0.0,
|
||||
"markPrice": 2896.41,
|
||||
# Collateral is in USDT - and can be higher than position size in cross mode
|
||||
"collateral": 50,
|
||||
"marginType": "cross",
|
||||
"side": "short",
|
||||
"percentage": None,
|
||||
}
|
||||
]
|
||||
|
||||
mocker.patch.multiple(
|
||||
"freqtrade.rpc.fiat_convert.FtCoinGeckoApi",
|
||||
get_price=MagicMock(return_value={"bitcoin": {"usd": 15000.0}}),
|
||||
)
|
||||
mocker.patch("freqtrade.rpc.rpc.CryptoToFiatConverter._find_price", return_value=15000.0)
|
||||
mocker.patch("freqtrade.rpc.telegram.Telegram", MagicMock())
|
||||
mocker.patch.multiple(
|
||||
EXMS,
|
||||
get_balances=MagicMock(return_value=mock_balance),
|
||||
fetch_positions=MagicMock(return_value=mock_pos),
|
||||
get_tickers=MagicMock(side_effect=TemporaryError("Could not load ticker due to xxx")),
|
||||
)
|
||||
|
||||
default_conf["trading_mode"] = "futures"
|
||||
default_conf["margin_mode"] = "isolated"
|
||||
default_conf["dry_run"] = False
|
||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||
patch_get_signal(freqtradebot)
|
||||
rpc = RPC(freqtradebot)
|
||||
rpc._fiat_converter = CryptoToFiatConverter({})
|
||||
mocker.patch.object(rpc._fiat_converter, "get_price", return_value=15000.0)
|
||||
res = rpc._rpc_balance(default_conf["stake_currency"], default_conf["fiat_display_currency"])
|
||||
assert res["stake"] == "BTC"
|
||||
|
||||
assert len(res["currencies"]) == 1
|
||||
assert len(res["currencies"]) == 3
|
||||
assert res["currencies"][0]["currency"] == "BTC"
|
||||
# ETH has not been converted.
|
||||
assert all(currency["currency"] != "ETH" for currency in res["currencies"])
|
||||
curr_ETH = next(currency for currency in res["currencies"] if currency["currency"] == "ETH")
|
||||
# coins are part of the result, but were not converted
|
||||
assert curr_ETH is not None
|
||||
assert curr_ETH["currency"] == "ETH"
|
||||
assert curr_ETH["est_stake"] == 0
|
||||
curr_ADA = next(
|
||||
currency for currency in res["currencies"] if currency["currency"] == "ADA/USDT:USDT"
|
||||
)
|
||||
assert curr_ADA is not None
|
||||
assert curr_ADA["currency"] == "ADA/USDT:USDT"
|
||||
# Fall back to collateral value when rate not available
|
||||
assert curr_ADA["est_stake"] == 20
|
||||
|
||||
assert log_has_re(r"Error .* getting rate for futures ADA.*", caplog)
|
||||
assert log_has_re(r"Error .* getting rate for ETH.*", caplog)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("proxy_coin", [None, "BNFCR"])
|
||||
@pytest.mark.parametrize("margin_mode", ["isolated", "cross"])
|
||||
def test_rpc_balance_handle(default_conf_usdt, mocker, tickers, proxy_coin, margin_mode):
|
||||
@pytest.mark.parametrize("is_short", [True, False])
|
||||
def test_rpc_balance_handle(default_conf_usdt, mocker, tickers, proxy_coin, margin_mode, is_short):
|
||||
mock_balance = {
|
||||
"BTC": {
|
||||
"free": 0.01,
|
||||
@@ -564,6 +609,8 @@ def test_rpc_balance_handle(default_conf_usdt, mocker, tickers, proxy_coin, marg
|
||||
"used": 5.0,
|
||||
},
|
||||
}
|
||||
# Fake ADA response
|
||||
tickers.return_value["ADA/USDT"] = tickers.return_value["ETH/USDT"]
|
||||
if proxy_coin:
|
||||
default_conf_usdt["proxy_coin"] = proxy_coin
|
||||
mock_balance[proxy_coin] = {
|
||||
@@ -572,12 +619,13 @@ def test_rpc_balance_handle(default_conf_usdt, mocker, tickers, proxy_coin, marg
|
||||
"used": 0.0,
|
||||
}
|
||||
|
||||
# Current ADA price based on Tickers is 530.21 USDT
|
||||
mock_pos = [
|
||||
{
|
||||
"symbol": "ETH/USDT:USDT",
|
||||
"symbol": "ADA/USDT:USDT",
|
||||
"timestamp": None,
|
||||
"datetime": None,
|
||||
"initialMargin": 20,
|
||||
"initialMargin": 50,
|
||||
"initialMarginPercentage": None,
|
||||
"maintenanceMargin": 0.0,
|
||||
"maintenanceMarginPercentage": 0.005,
|
||||
@@ -585,24 +633,19 @@ def test_rpc_balance_handle(default_conf_usdt, mocker, tickers, proxy_coin, marg
|
||||
"notional": 10.0,
|
||||
"leverage": 5.0,
|
||||
"unrealizedPnl": 0.0,
|
||||
"contracts": 1.0,
|
||||
"contracts": 0.48,
|
||||
"contractSize": 1,
|
||||
"marginRatio": None,
|
||||
"liquidationPrice": 0.0,
|
||||
"markPrice": 2896.41,
|
||||
"markPrice": 520, # Entry price ...
|
||||
# Collateral is in USDT - and can be higher than position size in cross mode
|
||||
"collateral": 50,
|
||||
"collateral": 100,
|
||||
"marginType": "cross",
|
||||
"side": "short",
|
||||
"side": "short" if is_short else "long",
|
||||
"percentage": None,
|
||||
}
|
||||
]
|
||||
|
||||
mocker.patch.multiple(
|
||||
"freqtrade.rpc.fiat_convert.FtCoinGeckoApi",
|
||||
get_price=MagicMock(return_value={"bitcoin": {"usd": 1.2}}),
|
||||
)
|
||||
mocker.patch("freqtrade.rpc.rpc.CryptoToFiatConverter._find_price", return_value=1.2)
|
||||
mocker.patch("freqtrade.rpc.telegram.Telegram", MagicMock())
|
||||
mocker.patch.multiple(
|
||||
EXMS,
|
||||
@@ -613,6 +656,7 @@ def test_rpc_balance_handle(default_conf_usdt, mocker, tickers, proxy_coin, marg
|
||||
get_valid_pair_combination=MagicMock(
|
||||
side_effect=lambda a, b: [f"{b}/{a}" if a == "USDT" else f"{a}/{b}"]
|
||||
),
|
||||
_contracts_to_amount=MagicMock(side_effect=lambda c, cs: cs),
|
||||
)
|
||||
default_conf_usdt["dry_run"] = False
|
||||
default_conf_usdt["trading_mode"] = "futures"
|
||||
@@ -621,15 +665,21 @@ def test_rpc_balance_handle(default_conf_usdt, mocker, tickers, proxy_coin, marg
|
||||
patch_get_signal(freqtradebot)
|
||||
rpc = RPC(freqtradebot)
|
||||
rpc._fiat_converter = CryptoToFiatConverter({})
|
||||
|
||||
mocker.patch.object(rpc._fiat_converter, "get_price", return_value=1.2)
|
||||
mocker.patch(
|
||||
"freqtrade.persistence.trade_model.Trade.get_open_trades",
|
||||
return_value=[
|
||||
MagicMock(pair="ADA/USDT:USDT", safe_base_currency="ADA"),
|
||||
],
|
||||
)
|
||||
result = rpc._rpc_balance(
|
||||
default_conf_usdt["stake_currency"], default_conf_usdt["fiat_display_currency"]
|
||||
)
|
||||
|
||||
assert tickers.call_count == 4 if not proxy_coin else 6
|
||||
assert tickers.call_count == (7 if proxy_coin and margin_mode != "cross" else 5)
|
||||
assert tickers.call_args_list[0][1]["cached"] is True
|
||||
# Testing futures - so we should get spot tickers
|
||||
assert tickers.call_args_list[-1][1]["market_type"] == "spot"
|
||||
tickers.assert_any_call(symbols=None, cached=True, market_type=TradingMode.SPOT)
|
||||
assert "USD" == result["symbol"]
|
||||
expected_curr = [
|
||||
{
|
||||
@@ -689,15 +739,15 @@ def test_rpc_balance_handle(default_conf_usdt, mocker, tickers, proxy_coin, marg
|
||||
"is_position": False,
|
||||
},
|
||||
{
|
||||
"currency": "ETH/USDT:USDT",
|
||||
"currency": "ADA/USDT:USDT",
|
||||
"free": 0,
|
||||
"balance": 0,
|
||||
"used": 0,
|
||||
"position": 10.0,
|
||||
"est_stake": 20,
|
||||
"est_stake_bot": 20,
|
||||
"position": 0.48,
|
||||
"est_stake": pytest.approx(45.4992 if is_short else 54.5008),
|
||||
"est_stake_bot": pytest.approx(45.4992 if is_short else 54.5008),
|
||||
"stake": "USDT",
|
||||
"side": "short",
|
||||
"side": "short" if is_short else "long",
|
||||
"is_bot_managed": True,
|
||||
"is_position": True,
|
||||
},
|
||||
@@ -757,18 +807,105 @@ def test_rpc_balance_handle(default_conf_usdt, mocker, tickers, proxy_coin, marg
|
||||
|
||||
assert result["currencies"] == expected_curr
|
||||
if proxy_coin and margin_mode == "cross":
|
||||
assert pytest.approx(result["total_bot"]) == 1505.0
|
||||
assert pytest.approx(result["total"]) == 2186.6972 # ETH stake is missing.
|
||||
# only USDT and ADA position are bot-managed
|
||||
assert pytest.approx(result["total_bot"]) == (1530.4992 if is_short else 1539.5008)
|
||||
assert pytest.approx(result["total"]) == (2212.19640 if is_short else 2221.198)
|
||||
assert result["starting_capital"] == 1500 * default_conf_usdt["tradable_balance_ratio"]
|
||||
assert result["starting_capital_ratio"] == pytest.approx(0.013468013468013407)
|
||||
assert result["starting_capital_ratio"] == pytest.approx(
|
||||
0.03063919 if is_short else 0.03670087
|
||||
)
|
||||
else:
|
||||
assert pytest.approx(result["total_bot"]) == 69.5
|
||||
assert pytest.approx(result["total"]) == 686.6972 # ETH stake is missing.
|
||||
# only USDT and ADA position are bot-managed
|
||||
assert pytest.approx(result["total_bot"]) == (94.9992 if is_short else 104.0008)
|
||||
assert pytest.approx(result["total"]) == (712.1964 if is_short else 721.1980)
|
||||
assert result["starting_capital"] == 50 * default_conf_usdt["tradable_balance_ratio"]
|
||||
assert result["starting_capital_ratio"] == pytest.approx(0.4040404)
|
||||
assert result["starting_capital_ratio"] == pytest.approx(0.919175 if is_short else 1.101026)
|
||||
assert pytest.approx(result["value"]) == result["total"] * 1.2
|
||||
|
||||
|
||||
def test_rpc_balance_futures(default_conf_usdt, mocker):
|
||||
"""Validate est_stake (equity) calculation for both short and long positions.
|
||||
|
||||
Short scenario:
|
||||
- collateral = 100, leverage = 2, position = 2, rate = 50
|
||||
- open_value = 200, current_value = 100 -> unlevered PnL = 100
|
||||
- equity = collateral + PnL = 200
|
||||
|
||||
Long scenario:
|
||||
- collateral = 150, leverage = 3, position = 3, rate = 200
|
||||
- open_value = 450, current_value = 600 -> unlevered PnL = 150
|
||||
- equity = collateral + PnL = 300
|
||||
"""
|
||||
from freqtrade.wallets import PositionWallet, Wallet
|
||||
|
||||
mock_balance = {"USDT": {"free": 1000.0, "total": 1000.0, "used": 0.0}}
|
||||
|
||||
# Patch exchange and wallets with different rates per base currency
|
||||
def _rate(base, stake):
|
||||
if base == "FOO":
|
||||
return 50.0
|
||||
if base == "BAR":
|
||||
return 200.0
|
||||
return None
|
||||
|
||||
mocker.patch.multiple(
|
||||
EXMS,
|
||||
validate_trading_mode_and_margin_mode=MagicMock(),
|
||||
get_balances=MagicMock(return_value=mock_balance),
|
||||
get_tickers=MagicMock(return_value={}),
|
||||
get_conversion_rate=MagicMock(side_effect=_rate),
|
||||
get_pair_base_currency=MagicMock(side_effect=lambda pair: pair.split("/")[0]),
|
||||
)
|
||||
|
||||
default_conf_usdt["dry_run"] = False
|
||||
default_conf_usdt["trading_mode"] = "futures"
|
||||
default_conf_usdt["margin_mode"] = "isolated"
|
||||
|
||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||
|
||||
# Create a short and a long position wallet directly to avoid depending on position parsing
|
||||
short_pos = PositionWallet(
|
||||
symbol="FOO/USDT:USDT",
|
||||
position=2.0,
|
||||
leverage=2.0,
|
||||
collateral=100.0,
|
||||
side="short",
|
||||
)
|
||||
long_pos = PositionWallet(
|
||||
symbol="BAR/USDT:USDT",
|
||||
position=3.0,
|
||||
leverage=3.0,
|
||||
collateral=150.0,
|
||||
side="long",
|
||||
)
|
||||
|
||||
mocker.patch.multiple(
|
||||
freqtradebot.wallets,
|
||||
get_all_positions=MagicMock(
|
||||
return_value={short_pos.symbol: short_pos, long_pos.symbol: long_pos}
|
||||
),
|
||||
get_all_balances=MagicMock(return_value={"USDT": Wallet("USDT", 1000.0, 1000.0, 0.0)}),
|
||||
)
|
||||
|
||||
rpc = RPC(freqtradebot)
|
||||
result = rpc._rpc_balance(
|
||||
default_conf_usdt["stake_currency"], default_conf_usdt["fiat_display_currency"]
|
||||
)
|
||||
|
||||
pos_short = next(c for c in result["currencies"] if c["currency"] == short_pos.symbol)
|
||||
pos_long = next(c for c in result["currencies"] if c["currency"] == long_pos.symbol)
|
||||
|
||||
assert pos_short["est_stake"] == 200.0
|
||||
assert pos_long["est_stake"] == 300.0
|
||||
assert result["total"] == 1500.0
|
||||
assert result["total_bot"] == 1490.0
|
||||
assert result["value_bot"] == 0 # No fiat conversion
|
||||
stake_pos = result["currencies"][0]
|
||||
assert stake_pos["currency"] == "USDT"
|
||||
assert stake_pos["est_stake_bot"] == 990.0
|
||||
assert stake_pos["bot_owned"] == 990.0
|
||||
|
||||
|
||||
def test_rpc_start(mocker, default_conf) -> None:
|
||||
mocker.patch("freqtrade.rpc.telegram.Telegram", MagicMock())
|
||||
mocker.patch.multiple(EXMS, fetch_ticker=MagicMock())
|
||||
@@ -848,11 +985,11 @@ def test_rpc_force_exit(default_conf, ticker, fee, mocker) -> None:
|
||||
|
||||
freqtradebot.state = State.STOPPED
|
||||
with pytest.raises(RPCException, match=r".*trader is not running*"):
|
||||
rpc._rpc_force_exit(None)
|
||||
rpc._rpc_force_exit("22222")
|
||||
|
||||
freqtradebot.state = State.RUNNING
|
||||
with pytest.raises(RPCException, match=r".*invalid argument*"):
|
||||
rpc._rpc_force_exit(None)
|
||||
rpc._rpc_force_exit("22222")
|
||||
|
||||
msg = rpc._rpc_force_exit("all")
|
||||
assert msg == {"result": "Created exit orders for all open trades."}
|
||||
@@ -867,7 +1004,7 @@ def test_rpc_force_exit(default_conf, ticker, fee, mocker) -> None:
|
||||
|
||||
freqtradebot.state = State.STOPPED
|
||||
with pytest.raises(RPCException, match=r".*trader is not running*"):
|
||||
rpc._rpc_force_exit(None)
|
||||
rpc._rpc_force_exit("22222")
|
||||
|
||||
with pytest.raises(RPCException, match=r".*trader is not running*"):
|
||||
rpc._rpc_force_exit("all")
|
||||
|
||||
+126
-21
@@ -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
|
||||
@@ -1033,8 +1034,7 @@ def test_api_delete_trade(botclient, mocker, fee, markets, is_short):
|
||||
stoploss_mock = MagicMock()
|
||||
cancel_mock = MagicMock()
|
||||
mocker.patch.multiple(
|
||||
EXMS,
|
||||
markets=PropertyMock(return_value=markets),
|
||||
ftbot.exchange,
|
||||
cancel_order=cancel_mock,
|
||||
cancel_stoploss_order=stoploss_mock,
|
||||
)
|
||||
@@ -1199,6 +1199,11 @@ def test_api_logs(botclient):
|
||||
"winrate": 0.0,
|
||||
"expectancy": -0.0033695635,
|
||||
"expectancy_ratio": -1.0,
|
||||
"cagr": -0.0024567404889381805,
|
||||
"calmar": -1910.497317469542,
|
||||
"sharpe": -58.138247358830355,
|
||||
"sortino": -58.138247358830355,
|
||||
"sqn": -1.5215,
|
||||
"trading_volume": 75.945,
|
||||
},
|
||||
),
|
||||
@@ -1231,6 +1236,11 @@ def test_api_logs(botclient):
|
||||
"winrate": 1.0,
|
||||
"expectancy": 0.0003695635,
|
||||
"expectancy_ratio": 100,
|
||||
"cagr": 0.0002698167695580622,
|
||||
"calmar": -100.0,
|
||||
"sharpe": 65.81269184917424,
|
||||
"sortino": -100.0,
|
||||
"sqn": 1.7224,
|
||||
"trading_volume": 75.945,
|
||||
},
|
||||
),
|
||||
@@ -1263,6 +1273,11 @@ def test_api_logs(botclient):
|
||||
"winrate": 0.5,
|
||||
"expectancy": -0.0027145635000000003,
|
||||
"expectancy_ratio": -0.48612137582114445,
|
||||
"cagr": -0.0019796559404918757,
|
||||
"calmar": -1857.4671689202785,
|
||||
"sharpe": -36.14602907243071,
|
||||
"sortino": -100.0,
|
||||
"sqn": -0.946,
|
||||
"trading_volume": 75.945,
|
||||
},
|
||||
),
|
||||
@@ -1326,6 +1341,11 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, is_short, expected)
|
||||
"winrate": expected["winrate"],
|
||||
"expectancy": expected["expectancy"],
|
||||
"expectancy_ratio": expected["expectancy_ratio"],
|
||||
"sharpe": expected["sharpe"],
|
||||
"sortino": expected["sortino"],
|
||||
"sqn": expected["sqn"],
|
||||
"calmar": expected["calmar"],
|
||||
"cagr": expected["cagr"],
|
||||
"max_drawdown": ANY,
|
||||
"max_drawdown_abs": ANY,
|
||||
"max_drawdown_start": ANY,
|
||||
@@ -1604,6 +1624,8 @@ def test_api_status(
|
||||
"precision_mode": None,
|
||||
"orders": [ANY],
|
||||
"has_open_orders": True,
|
||||
"nr_of_successful_entries": ANY,
|
||||
"nr_of_successful_exits": ANY,
|
||||
}
|
||||
|
||||
mocker.patch(
|
||||
@@ -1816,6 +1838,8 @@ def test_api_force_entry(botclient, mocker, fee, endpoint):
|
||||
"price_precision": None,
|
||||
"precision_mode": None,
|
||||
"has_open_orders": False,
|
||||
"nr_of_successful_entries": ANY,
|
||||
"nr_of_successful_exits": ANY,
|
||||
"orders": [],
|
||||
}
|
||||
|
||||
@@ -1848,9 +1872,35 @@ def test_api_forceexit(botclient, mocker, ticker, fee, markets):
|
||||
Trade.rollback()
|
||||
|
||||
trade = Trade.get_trades([Trade.id == 5]).first()
|
||||
last_order = trade.orders[-1]
|
||||
|
||||
assert last_order.side == "sell"
|
||||
assert last_order.status == "closed"
|
||||
assert last_order.order_type == "market"
|
||||
assert last_order.amount == 23
|
||||
assert pytest.approx(trade.amount) == 100
|
||||
assert trade.is_open is True
|
||||
|
||||
# Test with explicit price
|
||||
rc = client_post(
|
||||
client,
|
||||
f"{BASE_URI}/forceexit",
|
||||
data={"tradeid": "5", "ordertype": "limit", "amount": 25, "price": 0.12345},
|
||||
)
|
||||
assert_response(rc)
|
||||
assert rc.json() == {"result": "Created exit order for trade 5."}
|
||||
Trade.rollback()
|
||||
|
||||
trade = Trade.get_trades([Trade.id == 5]).first()
|
||||
last_order = trade.orders[-1]
|
||||
assert last_order.status == "closed"
|
||||
assert last_order.order_type == "limit"
|
||||
assert pytest.approx(last_order.safe_price) == 0.12345
|
||||
assert pytest.approx(last_order.amount) == 25
|
||||
|
||||
assert pytest.approx(trade.amount) == 75
|
||||
assert trade.is_open is True
|
||||
|
||||
rc = client_post(client, f"{BASE_URI}/forceexit", data={"tradeid": "5"})
|
||||
assert_response(rc)
|
||||
assert rc.json() == {"result": "Created exit order for trade 5."}
|
||||
@@ -1860,7 +1910,60 @@ 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",
|
||||
}
|
||||
point_annotation = {
|
||||
"type": "point",
|
||||
"x": "2024-01-01 15:30:00",
|
||||
"y": 97000,
|
||||
"color": "",
|
||||
"label": "some label",
|
||||
"size": 10,
|
||||
"shape": "circle",
|
||||
}
|
||||
|
||||
line_wrong = deepcopy(line_annotation)
|
||||
line_wrong["line_style"] = "dashed2222"
|
||||
point_wrong = deepcopy(point_annotation)
|
||||
point_wrong["shape"] = "circle2222"
|
||||
# annotations / expected
|
||||
return [
|
||||
([area_annotation], [area_annotation]), # Only area
|
||||
([line_annotation], [line_annotation]), # Only line
|
||||
([point_annotation], [point_annotation]), # Only point
|
||||
([area_annotation, line_annotation], [area_annotation, line_annotation]), # mark and line
|
||||
(
|
||||
[area_annotation, line_annotation, point_annotation],
|
||||
[area_annotation, line_annotation, point_annotation],
|
||||
), # all together
|
||||
([], []), # Empty
|
||||
([line_wrong], []), # Invalid line
|
||||
([area_annotation, line_wrong], [area_annotation]), # Invalid line
|
||||
([point_wrong], []), # Invalid point
|
||||
]
|
||||
|
||||
|
||||
@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 +1995,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 +2028,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 = {
|
||||
@@ -2434,6 +2526,7 @@ def test_api_plot_config(botclient, mocker, tmp_path):
|
||||
def test_api_strategies(botclient, tmp_path):
|
||||
ftbot, client = botclient
|
||||
ftbot.config["user_data_dir"] = tmp_path
|
||||
ftbot.config["runmode"] = RunMode.WEBSERVER
|
||||
|
||||
rc = client_get(client, f"{BASE_URI}/strategies")
|
||||
|
||||
@@ -2459,15 +2552,18 @@ def test_api_strategies(botclient, tmp_path):
|
||||
|
||||
|
||||
def test_api_strategy(botclient, tmp_path, mocker):
|
||||
_ftbot, client = botclient
|
||||
_ftbot.config["user_data_dir"] = tmp_path
|
||||
ftbot, client = botclient
|
||||
ftbot.config["user_data_dir"] = tmp_path
|
||||
ftbot.config["runmode"] = RunMode.WEBSERVER
|
||||
|
||||
rc = client_get(client, f"{BASE_URI}/strategy/{CURRENT_TEST_STRATEGY}")
|
||||
|
||||
assert_response(rc)
|
||||
assert rc.json()["strategy"] == CURRENT_TEST_STRATEGY
|
||||
|
||||
data = (Path(__file__).parents[1] / "strategy/strats/strategy_test_v3.py").read_text()
|
||||
data = (Path(__file__).parents[1] / "strategy/strats/strategy_test_v3.py").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
assert rc.json()["code"] == data
|
||||
|
||||
rc = client_get(client, f"{BASE_URI}/strategy/NoStrat")
|
||||
@@ -2487,6 +2583,7 @@ def test_api_strategy(botclient, tmp_path, mocker):
|
||||
|
||||
def test_api_exchanges(botclient):
|
||||
_ftbot, client = botclient
|
||||
_ftbot.config["runmode"] = RunMode.WEBSERVER
|
||||
|
||||
rc = client_get(client, f"{BASE_URI}/exchanges")
|
||||
assert_response(rc)
|
||||
@@ -2500,6 +2597,7 @@ def test_api_exchanges(botclient):
|
||||
"valid": True,
|
||||
"supported": True,
|
||||
"comment": "",
|
||||
"comment_futures": ANY,
|
||||
"dex": False,
|
||||
"is_alias": False,
|
||||
"alias_for": None,
|
||||
@@ -2517,6 +2615,7 @@ def test_api_exchanges(botclient):
|
||||
"supported": False,
|
||||
"dex": False,
|
||||
"comment": "",
|
||||
"comment_futures": ANY,
|
||||
"is_alias": False,
|
||||
"alias_for": None,
|
||||
"trade_modes": [{"trading_mode": "spot", "margin_mode": ""}],
|
||||
@@ -2529,6 +2628,7 @@ def test_api_exchanges(botclient):
|
||||
"supported": False,
|
||||
"dex": True,
|
||||
"comment": ANY,
|
||||
"comment_futures": ANY,
|
||||
"is_alias": False,
|
||||
"alias_for": None,
|
||||
"trade_modes": [{"trading_mode": "spot", "margin_mode": ""}],
|
||||
@@ -2538,6 +2638,7 @@ def test_api_exchanges(botclient):
|
||||
def test_list_hyperoptloss(botclient, tmp_path):
|
||||
ftbot, client = botclient
|
||||
ftbot.config["user_data_dir"] = tmp_path
|
||||
ftbot.config["runmode"] = RunMode.WEBSERVER
|
||||
|
||||
rc = client_get(client, f"{BASE_URI}/hyperoptloss")
|
||||
assert_response(rc)
|
||||
@@ -2554,6 +2655,8 @@ def test_list_hyperoptloss(botclient, tmp_path):
|
||||
def test_api_freqaimodels(botclient, tmp_path, mocker):
|
||||
ftbot, client = botclient
|
||||
ftbot.config["user_data_dir"] = tmp_path
|
||||
ftbot.config["runmode"] = RunMode.WEBSERVER
|
||||
|
||||
mocker.patch(
|
||||
"freqtrade.resolvers.freqaimodel_resolver.FreqaiModelResolver.search_all_objects",
|
||||
return_value=[
|
||||
@@ -2725,16 +2828,17 @@ def test_api_pairlists_evaluate(botclient, tmp_path, mocker):
|
||||
|
||||
def test_list_available_pairs(botclient):
|
||||
ftbot, client = botclient
|
||||
ftbot.config["runmode"] = RunMode.WEBSERVER
|
||||
|
||||
rc = client_get(client, f"{BASE_URI}/available_pairs")
|
||||
|
||||
assert_response(rc)
|
||||
assert rc.json()["length"] == 12
|
||||
assert rc.json()["length"] == 14
|
||||
assert isinstance(rc.json()["pairs"], list)
|
||||
|
||||
rc = client_get(client, f"{BASE_URI}/available_pairs?timeframe=5m")
|
||||
assert_response(rc)
|
||||
assert rc.json()["length"] == 12
|
||||
assert rc.json()["length"] == 14
|
||||
|
||||
rc = client_get(client, f"{BASE_URI}/available_pairs?stake_currency=ETH")
|
||||
assert_response(rc)
|
||||
@@ -3222,6 +3326,7 @@ def test_api_download_data(botclient, mocker, tmp_path):
|
||||
body = {
|
||||
"pairs": ["ETH/BTC", "XRP/BTC"],
|
||||
"timeframes": ["5m"],
|
||||
"candle_types": ["spot"],
|
||||
}
|
||||
|
||||
# Fail, already running
|
||||
|
||||
@@ -119,7 +119,7 @@ class DummyCls(Telegram):
|
||||
raise Exception("test")
|
||||
|
||||
|
||||
def get_telegram_testobject(mocker, default_conf, mock=True, ftbot=None):
|
||||
def get_telegram_testobject(mocker, default_conf, mock=True, ftbot=None, mock_fiat=True):
|
||||
msg_mock = AsyncMock()
|
||||
if mock:
|
||||
mocker.patch.multiple(
|
||||
@@ -131,6 +131,9 @@ def get_telegram_testobject(mocker, default_conf, mock=True, ftbot=None):
|
||||
if not ftbot:
|
||||
ftbot = get_patched_freqtradebot(mocker, default_conf)
|
||||
rpc = RPC(ftbot)
|
||||
if rpc._fiat_converter is not None and mock_fiat:
|
||||
mocker.patch.object(rpc._fiat_converter, "get_price", return_value=1.1)
|
||||
|
||||
telegram = Telegram(rpc, default_conf)
|
||||
telegram._loop = MagicMock()
|
||||
patch_eventloop_threading(telegram)
|
||||
@@ -421,7 +424,8 @@ async def test_telegram_status_multi_entry(default_conf, update, mocker, fee) ->
|
||||
assert msg_mock.call_count == 4
|
||||
msg = msg_mock.call_args_list[3][0][0]
|
||||
assert re.search(r"Number of Entries.*2", msg)
|
||||
assert re.search(r"Number of Exits.*1", msg)
|
||||
# Exit order is still open, hence not a successful exit
|
||||
assert re.search(r"Number of Exits.*0", msg)
|
||||
assert re.search(r"Close Date:", msg) is None
|
||||
assert re.search(r"Close Profit:", msg) is None
|
||||
|
||||
@@ -666,7 +670,6 @@ async def test_status_table_handle(default_conf, update, ticker, fee, mocker) ->
|
||||
|
||||
|
||||
async def test_daily_handle(default_conf_usdt, update, ticker, fee, mocker, time_machine) -> None:
|
||||
mocker.patch("freqtrade.rpc.rpc.CryptoToFiatConverter._find_price", return_value=1.1)
|
||||
mocker.patch.multiple(
|
||||
EXMS,
|
||||
fetch_ticker=ticker,
|
||||
@@ -749,7 +752,6 @@ async def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None:
|
||||
|
||||
async def test_weekly_handle(default_conf_usdt, update, ticker, fee, mocker, time_machine) -> None:
|
||||
default_conf_usdt["max_open_trades"] = 1
|
||||
mocker.patch("freqtrade.rpc.rpc.CryptoToFiatConverter._find_price", return_value=1.1)
|
||||
mocker.patch.multiple(
|
||||
EXMS,
|
||||
fetch_ticker=ticker,
|
||||
@@ -820,7 +822,6 @@ async def test_weekly_handle(default_conf_usdt, update, ticker, fee, mocker, tim
|
||||
|
||||
async def test_monthly_handle(default_conf_usdt, update, ticker, fee, mocker, time_machine) -> None:
|
||||
default_conf_usdt["max_open_trades"] = 1
|
||||
mocker.patch("freqtrade.rpc.rpc.CryptoToFiatConverter._find_price", return_value=1.1)
|
||||
mocker.patch.multiple(
|
||||
EXMS,
|
||||
fetch_ticker=ticker,
|
||||
@@ -902,7 +903,6 @@ async def test_monthly_handle(default_conf_usdt, update, ticker, fee, mocker, ti
|
||||
async def test_telegram_profit_handle(
|
||||
default_conf_usdt, update, ticker_usdt, ticker_sell_up, fee, limit_sell_order_usdt, mocker
|
||||
) -> None:
|
||||
mocker.patch("freqtrade.rpc.rpc.CryptoToFiatConverter._find_price", return_value=1.1)
|
||||
mocker.patch.multiple(
|
||||
EXMS,
|
||||
fetch_ticker=ticker_usdt,
|
||||
@@ -982,7 +982,6 @@ async def test_telegram_profit_long_short_handle(
|
||||
is consistent with /profit, covering both no trades and trades present cases.
|
||||
"""
|
||||
|
||||
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)
|
||||
|
||||
@@ -1061,7 +1060,6 @@ async def test_telegram_profit_long_short_handle(
|
||||
|
||||
@pytest.mark.parametrize("is_short", [True, False])
|
||||
async def test_telegram_stats(default_conf, update, ticker, fee, mocker, is_short) -> None:
|
||||
mocker.patch("freqtrade.rpc.rpc.CryptoToFiatConverter._find_price", return_value=15000.0)
|
||||
mocker.patch.multiple(
|
||||
EXMS,
|
||||
fetch_ticker=ticker,
|
||||
@@ -1157,7 +1155,7 @@ async def test_telegram_balance_handle_futures(
|
||||
"percentage": None,
|
||||
},
|
||||
{
|
||||
"symbol": "XRP/USDT:USDT",
|
||||
"symbol": "ADA/USDT:USDT",
|
||||
"timestamp": None,
|
||||
"datetime": None,
|
||||
"initialMargin": 0.0,
|
||||
@@ -1183,9 +1181,17 @@ async def test_telegram_balance_handle_futures(
|
||||
mocker.patch(f"{EXMS}.fetch_positions", return_value=mock_pos)
|
||||
mocker.patch(f"{EXMS}.get_tickers", tickers)
|
||||
mocker.patch(f"{EXMS}.get_valid_pair_combination", side_effect=lambda a, b: [f"{a}/{b}"])
|
||||
mocker.patch(f"{EXMS}.get_conversion_rate", return_value=3200)
|
||||
|
||||
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
|
||||
patch_get_signal(freqtradebot)
|
||||
mocker.patch(
|
||||
"freqtrade.persistence.trade_model.Trade.get_open_trades",
|
||||
return_value=[
|
||||
MagicMock(pair="ETH/USDT:USDT", safe_base_currency="ETH"),
|
||||
MagicMock(pair="ADA/USDT:USDT", safe_base_currency="ADA"),
|
||||
],
|
||||
)
|
||||
|
||||
await telegram._balance(update=update, context=MagicMock())
|
||||
result = msg_mock.call_args_list[0][0][0]
|
||||
@@ -1193,7 +1199,7 @@ async def test_telegram_balance_handle_futures(
|
||||
|
||||
assert "ETH/USDT:USDT" in result
|
||||
assert "`short: 10" in result
|
||||
assert "XRP/USDT:USDT" in result
|
||||
assert "ADA/USDT:USDT" in result
|
||||
|
||||
|
||||
async def test_balance_handle_empty_response(default_conf, update, mocker) -> None:
|
||||
@@ -1340,7 +1346,6 @@ async def test_reload_config_handle(default_conf, update, mocker) -> None:
|
||||
async def test_telegram_forceexit_handle(
|
||||
default_conf, update, ticker, fee, ticker_sell_up, mocker
|
||||
) -> None:
|
||||
mocker.patch("freqtrade.rpc.rpc.CryptoToFiatConverter._find_price", return_value=15000.0)
|
||||
msg_mock = mocker.patch("freqtrade.rpc.telegram.Telegram.send_msg", MagicMock())
|
||||
mocker.patch("freqtrade.rpc.telegram.Telegram._init", MagicMock())
|
||||
patch_exchange(mocker)
|
||||
@@ -1410,9 +1415,6 @@ async def test_telegram_forceexit_handle(
|
||||
async def test_telegram_force_exit_down_handle(
|
||||
default_conf, update, ticker, fee, ticker_sell_down, mocker
|
||||
) -> None:
|
||||
mocker.patch(
|
||||
"freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price", return_value=15000.0
|
||||
)
|
||||
msg_mock = mocker.patch("freqtrade.rpc.telegram.Telegram.send_msg", MagicMock())
|
||||
mocker.patch("freqtrade.rpc.telegram.Telegram._init", MagicMock())
|
||||
patch_exchange(mocker)
|
||||
@@ -1483,9 +1485,6 @@ async def test_telegram_force_exit_down_handle(
|
||||
|
||||
async def test_forceexit_all_handle(default_conf, update, ticker, fee, mocker) -> None:
|
||||
patch_exchange(mocker)
|
||||
mocker.patch(
|
||||
"freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price", return_value=15000.0
|
||||
)
|
||||
msg_mock = mocker.patch("freqtrade.rpc.telegram.Telegram.send_msg", MagicMock())
|
||||
mocker.patch("freqtrade.rpc.telegram.Telegram._init", MagicMock())
|
||||
patch_whitelist(mocker, default_conf)
|
||||
@@ -1548,10 +1547,6 @@ async def test_forceexit_all_handle(default_conf, update, ticker, fee, mocker) -
|
||||
|
||||
|
||||
async def test_forceexit_handle_invalid(default_conf, update, mocker) -> None:
|
||||
mocker.patch(
|
||||
"freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price", return_value=15000.0
|
||||
)
|
||||
|
||||
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
|
||||
patch_get_signal(freqtradebot)
|
||||
|
||||
@@ -1629,8 +1624,6 @@ async def test_force_exit_no_pair(default_conf, update, ticker, fee, mocker) ->
|
||||
|
||||
|
||||
async def test_force_enter_handle(default_conf, update, mocker) -> None:
|
||||
mocker.patch("freqtrade.rpc.rpc.CryptoToFiatConverter._find_price", return_value=15000.0)
|
||||
|
||||
fbuy_mock = MagicMock(return_value=None)
|
||||
mocker.patch("freqtrade.rpc.rpc.RPC._rpc_force_entry", fbuy_mock)
|
||||
|
||||
@@ -1662,8 +1655,6 @@ async def test_force_enter_handle(default_conf, update, mocker) -> None:
|
||||
|
||||
|
||||
async def test_force_enter_handle_exception(default_conf, update, mocker) -> None:
|
||||
mocker.patch("freqtrade.rpc.rpc.CryptoToFiatConverter._find_price", return_value=15000.0)
|
||||
|
||||
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
|
||||
patch_get_signal(freqtradebot)
|
||||
|
||||
@@ -1674,10 +1665,7 @@ async def test_force_enter_handle_exception(default_conf, update, mocker) -> Non
|
||||
|
||||
|
||||
async def test_force_enter_no_pair(default_conf, update, mocker) -> None:
|
||||
mocker.patch("freqtrade.rpc.rpc.CryptoToFiatConverter._find_price", return_value=15000.0)
|
||||
|
||||
fbuy_mock = MagicMock(return_value=None)
|
||||
mocker.patch("freqtrade.rpc.rpc.RPC._rpc_force_entry", fbuy_mock)
|
||||
fbuy_mock = mocker.patch("freqtrade.rpc.rpc.RPC._rpc_force_entry", return_value=None)
|
||||
|
||||
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
|
||||
|
||||
@@ -2240,7 +2228,9 @@ def test_send_msg_enter_notification(
|
||||
"analyzed_candle": {"open": 1.1, "high": 2.2, "low": 1.0, "close": 1.5},
|
||||
"open_date": dt_now() + timedelta(hours=-1),
|
||||
}
|
||||
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
|
||||
telegram, freqtradebot, msg_mock = get_telegram_testobject(
|
||||
mocker, default_conf, mock_fiat=False
|
||||
)
|
||||
|
||||
telegram.send_msg(msg)
|
||||
leverage_text = f" ({leverage:.3g}x)" if leverage and leverage != 1.0 else ""
|
||||
@@ -2346,7 +2336,7 @@ def test_send_msg_entry_fill_notification(
|
||||
default_conf, mocker, message_type, entered, enter_signal, leverage
|
||||
) -> None:
|
||||
default_conf["telegram"]["notification_settings"]["entry_fill"] = "on"
|
||||
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
|
||||
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf, mock_fiat=False)
|
||||
|
||||
telegram.send_msg(
|
||||
{
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
|
||||
|
||||
import logging
|
||||
|
||||
from pandas import DataFrame
|
||||
from strategy_test_v3 import StrategyTestV3
|
||||
|
||||
@@ -7,6 +9,9 @@ import freqtrade.vendor.qtpylib.indicators as qtpylib
|
||||
from freqtrade.strategy import BooleanParameter, DecimalParameter, IntParameter, RealParameter
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HyperoptableStrategy(StrategyTestV3):
|
||||
"""
|
||||
Default Strategy provided by freqtrade bot.
|
||||
@@ -16,6 +21,7 @@ class HyperoptableStrategy(StrategyTestV3):
|
||||
for samples and inspiration.
|
||||
"""
|
||||
|
||||
INTERFACE_VERSION = 3
|
||||
buy_params = {
|
||||
"buy_rsi": 35,
|
||||
# Intentionally not specified, so "default" is tested
|
||||
@@ -29,6 +35,9 @@ class HyperoptableStrategy(StrategyTestV3):
|
||||
sell_minusdi = DecimalParameter(
|
||||
low=0, high=1, default=0.5001, decimals=3, space="sell", load=False
|
||||
)
|
||||
exitaaa = IntParameter(low=0, high=10, default=5, space="exitaspace")
|
||||
|
||||
exit_rsi = IntParameter(low=0, high=10, default=5)
|
||||
protection_enabled = BooleanParameter(default=True)
|
||||
protection_cooldown_lookback = IntParameter([0, 50], default=30)
|
||||
|
||||
@@ -54,34 +63,13 @@ class HyperoptableStrategy(StrategyTestV3):
|
||||
|
||||
def bot_loop_start(self, **kwargs):
|
||||
self.bot_loop_started = True
|
||||
logger.info("Test: Bot loop started")
|
||||
|
||||
def bot_start(self, **kwargs) -> None:
|
||||
"""
|
||||
Parameters can also be defined here ...
|
||||
"""
|
||||
self.bot_started = True
|
||||
self.buy_rsi = IntParameter([0, 50], default=30, space="buy")
|
||||
|
||||
def informative_pairs(self):
|
||||
"""
|
||||
Define additional, informative pair/interval combinations to be cached from the exchange.
|
||||
These pair/interval combinations are non-tradeable, unless they are part
|
||||
of the whitelist as well.
|
||||
For more information, please consult the documentation
|
||||
:return: List of tuples in the format (pair, interval)
|
||||
Sample: return [("ETH/USDT", "5m"),
|
||||
("BTC/USDT", "15m"),
|
||||
]
|
||||
"""
|
||||
return []
|
||||
|
||||
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
"""
|
||||
Based on TA indicators, populates the buy signal for the given dataframe
|
||||
:param dataframe: DataFrame
|
||||
:param metadata: Additional information, like the currently traded pair
|
||||
:return: DataFrame with buy column
|
||||
"""
|
||||
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
dataframe.loc[
|
||||
(
|
||||
(dataframe["rsi"] < self.buy_rsi.value)
|
||||
@@ -90,18 +78,12 @@ class HyperoptableStrategy(StrategyTestV3):
|
||||
& (dataframe["plus_di"] > self.buy_plusdi.value)
|
||||
)
|
||||
| ((dataframe["adx"] > 65) & (dataframe["plus_di"] > self.buy_plusdi.value)),
|
||||
"buy",
|
||||
"enter_long",
|
||||
] = 1
|
||||
|
||||
return dataframe
|
||||
|
||||
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
"""
|
||||
Based on TA indicators, populates the sell signal for the given dataframe
|
||||
:param dataframe: DataFrame
|
||||
:param metadata: Additional information, like the currently traded pair
|
||||
:return: DataFrame with sell column
|
||||
"""
|
||||
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
dataframe.loc[
|
||||
(
|
||||
(
|
||||
@@ -112,6 +94,6 @@ class HyperoptableStrategy(StrategyTestV3):
|
||||
& (dataframe["minus_di"] > 0)
|
||||
)
|
||||
| ((dataframe["adx"] > 70) & (dataframe["minus_di"] > self.sell_minusdi.value)),
|
||||
"sell",
|
||||
"exit_long",
|
||||
] = 1
|
||||
return dataframe
|
||||
|
||||
@@ -14,6 +14,16 @@ class StrategyTestV2(IStrategy):
|
||||
Please look at the SampleStrategy in the user_data/strategy directory
|
||||
or strategy repository https://github.com/freqtrade/freqtrade-strategies
|
||||
for samples and inspiration.
|
||||
|
||||
---
|
||||
|
||||
Some test asian characters.
|
||||
Ensures that unicode characters are handled correctly when reading strategy files.
|
||||
Otherwise this may break on windows systems.
|
||||
All roughly translate to "hello world".
|
||||
chinese string: "你好世界"
|
||||
korean string: "안녕하세요,세계"
|
||||
japanese string: "こんにちは、世界"
|
||||
"""
|
||||
|
||||
INTERFACE_VERSION = 2
|
||||
|
||||
@@ -23,6 +23,16 @@ class StrategyTestV3(IStrategy):
|
||||
Please look at the SampleStrategy in the user_data/strategy directory
|
||||
or strategy repository https://github.com/freqtrade/freqtrade-strategies
|
||||
for samples and inspiration.
|
||||
|
||||
---
|
||||
|
||||
Some test asian characters.
|
||||
Ensures that unicode characters are handled correctly when reading strategy files.
|
||||
Otherwise this may break on windows systems.
|
||||
All roughly translate to "hello world".
|
||||
chinese string: "你好世界"
|
||||
korean string: "안녕하세요,세계"
|
||||
japanese string: "こんにちは、世界"
|
||||
"""
|
||||
|
||||
INTERFACE_VERSION = 3
|
||||
|
||||
@@ -16,7 +16,7 @@ from freqtrade.enums import ExitCheckTuple, ExitType, SignalDirection
|
||||
from freqtrade.exceptions import OperationalException, StrategyError
|
||||
from freqtrade.persistence import PairLocks, Trade
|
||||
from freqtrade.resolvers import StrategyResolver
|
||||
from freqtrade.strategy.hyper import detect_parameters
|
||||
from freqtrade.strategy.hyper import detect_all_parameters
|
||||
from freqtrade.strategy.parameters import (
|
||||
IntParameter,
|
||||
)
|
||||
@@ -147,14 +147,14 @@ def test_get_signal_exception_valueerror(mocker, caplog, ohlcv_history):
|
||||
mocker.patch.object(_STRATEGY.dp, "ohlcv", return_value=ohlcv_history)
|
||||
mocker.patch.object(_STRATEGY, "_analyze_ticker_internal", side_effect=ValueError("xyz"))
|
||||
_STRATEGY.analyze_pair("foo")
|
||||
assert log_has_re(r"Strategy caused the following exception: xyz.*", caplog)
|
||||
assert log_has_re(r"Strategy caused the following exception: ValueError\('xyz'\).*", caplog)
|
||||
caplog.clear()
|
||||
|
||||
mocker.patch.object(
|
||||
_STRATEGY, "analyze_ticker", side_effect=Exception("invalid ticker history ")
|
||||
)
|
||||
_STRATEGY.analyze_pair("foo")
|
||||
assert log_has_re(r"Strategy caused the following exception: xyz.*", caplog)
|
||||
assert log_has_re(r"Strategy caused the following exception: ValueError\('xyz'\).*", caplog)
|
||||
|
||||
|
||||
def test_get_signal_old_dataframe(default_conf, mocker, caplog, ohlcv_history):
|
||||
@@ -928,8 +928,7 @@ def test_auto_hyperopt_interface(default_conf):
|
||||
PairLocks.timeframe = default_conf["timeframe"]
|
||||
strategy = StrategyResolver.load_strategy(default_conf)
|
||||
strategy.ft_bot_start()
|
||||
with pytest.raises(OperationalException):
|
||||
next(strategy.enumerate_parameters("deadBeef"))
|
||||
assert list(strategy.enumerate_parameters("deadBeef")) == []
|
||||
|
||||
assert strategy.buy_rsi.value == strategy.buy_params["buy_rsi"]
|
||||
# PlusDI is NOT in the buy-params, so default should be used
|
||||
@@ -940,20 +939,52 @@ def test_auto_hyperopt_interface(default_conf):
|
||||
|
||||
# Parameter is disabled - so value from sell_param dict will NOT be used.
|
||||
assert strategy.sell_minusdi.value == 0.5
|
||||
all_params = strategy.detect_all_parameters()
|
||||
all_params = detect_all_parameters(strategy.__class__)
|
||||
assert isinstance(all_params, dict)
|
||||
# Only one buy param at class level
|
||||
assert len(all_params["buy"]) == 1
|
||||
# Running detect params at instance level reveals both parameters.
|
||||
assert len(list(detect_parameters(strategy, "buy"))) == 2
|
||||
assert len(all_params["sell"]) == 2
|
||||
# Number of Hyperoptable parameters
|
||||
assert all_params["count"] == 5
|
||||
params_inst = detect_all_parameters(strategy)
|
||||
assert len(params_inst["buy"]) == 2
|
||||
assert len(params_inst["sell"]) == 2
|
||||
|
||||
strategy.__class__.sell_rsi = IntParameter([0, 10], default=5, space="buy")
|
||||
|
||||
with pytest.raises(OperationalException, match=r"Inconclusive parameter.*"):
|
||||
[x for x in detect_parameters(strategy, "sell")]
|
||||
spaces = detect_all_parameters(strategy.__class__)
|
||||
assert "buy" in spaces
|
||||
assert spaces["buy"]["sell_rsi"] == strategy.sell_rsi
|
||||
del strategy.__class__.sell_rsi
|
||||
|
||||
strategy.__class__.exit22_rsi = IntParameter([0, 10], default=5)
|
||||
|
||||
with pytest.raises(
|
||||
OperationalException, match=r"Cannot determine parameter space for exit22_rsi\."
|
||||
):
|
||||
detect_all_parameters(strategy.__class__)
|
||||
|
||||
# Invalid parameter space
|
||||
strategy.__class__.exit22_rsi = IntParameter([0, 10], default=5, space="all")
|
||||
with pytest.raises(
|
||||
OperationalException, match=r"'all' is not a valid space\. Parameter: exit22_rsi\."
|
||||
):
|
||||
detect_all_parameters(strategy.__class__)
|
||||
|
||||
strategy.__class__.exit22_rsi = IntParameter([0, 10], default=5, space="hello:world:22")
|
||||
with pytest.raises(
|
||||
OperationalException,
|
||||
match=r"'hello:world:22' is not a valid space\. Parameter: exit22_rsi\.",
|
||||
):
|
||||
detect_all_parameters(strategy.__class__)
|
||||
del strategy.__class__.exit22_rsi
|
||||
|
||||
# Valid exit parameter
|
||||
strategy.__class__.exit_rsi = IntParameter([0, 10], default=5)
|
||||
strategy.__class__.enter_rsi = IntParameter([0, 10], default=5)
|
||||
spaces = detect_all_parameters(strategy.__class__)
|
||||
assert "exit" in spaces
|
||||
assert "enter" in spaces
|
||||
del strategy.__class__.exit_rsi
|
||||
del strategy.__class__.enter_rsi
|
||||
|
||||
|
||||
def test_auto_hyperopt_interface_loadparams(default_conf, mocker, caplog):
|
||||
@@ -1016,7 +1047,7 @@ def test_pandas_warning_direct(ohlcv_history, function, raises, recwarn):
|
||||
# Fixed in 2.2.x
|
||||
getattr(_STRATEGY, function)(df, {"pair": "ETH/BTC"})
|
||||
else:
|
||||
assert len(recwarn) == 0
|
||||
assert len(recwarn) == 0, f"warnings: {', '.join(recwarn.list)}"
|
||||
|
||||
getattr(_STRATEGY, function)(df, {"pair": "ETH/BTC"})
|
||||
|
||||
@@ -1024,4 +1055,4 @@ def test_pandas_warning_direct(ohlcv_history, function, raises, recwarn):
|
||||
def test_pandas_warning_through_analyze_pair(ohlcv_history, mocker, recwarn):
|
||||
mocker.patch.object(_STRATEGY.dp, "ohlcv", return_value=ohlcv_history)
|
||||
_STRATEGY.analyze_pair("ETH/BTC")
|
||||
assert len(recwarn) == 0
|
||||
assert len(recwarn) == 0, f"warnings: {', '.join(recwarn.list)}"
|
||||
|
||||
@@ -34,7 +34,8 @@ def test_merge_informative_pair():
|
||||
assert "volume_1h" in result.columns
|
||||
assert result["volume"].equals(data["volume"])
|
||||
|
||||
# First 3 rows are empty
|
||||
# First 3 rows are empty.
|
||||
# Pre-fillup doesn't happen as there is no prior candlw in the informative dataframe
|
||||
assert result.iloc[0]["date_1h"] is pd.NaT
|
||||
assert result.iloc[1]["date_1h"] is pd.NaT
|
||||
assert result.iloc[2]["date_1h"] is pd.NaT
|
||||
@@ -109,13 +110,37 @@ def test_merge_informative_pair_monthly():
|
||||
# Candle is empty, as the start-date did fail.
|
||||
candle3 = result.loc[(result["date"] == "2022-11-30T22:00:00.000Z")]
|
||||
assert candle3.iloc[0]["date"] == pd.Timestamp("2022-11-30T22:00:00.000Z")
|
||||
assert candle3.iloc[0]["date_1M"] is pd.NaT
|
||||
# Merged on prior month
|
||||
assert candle3.iloc[0]["date_1M"] == pd.Timestamp("2022-10-01T00:00:00.000Z")
|
||||
|
||||
# First candle with 1M data merged.
|
||||
candle4 = result.loc[(result["date"] == "2022-11-30T23:00:00.000Z")]
|
||||
assert candle4.iloc[0]["date"] == pd.Timestamp("2022-11-30T23:00:00.000Z")
|
||||
assert candle4.iloc[0]["date_1M"] == pd.Timestamp("2022-11-01T00:00:00.000Z")
|
||||
|
||||
# Very first candle in the result dataframe
|
||||
# Merged the latest informative candle before the start-date
|
||||
candle5 = result.iloc[0]
|
||||
assert candle5["date"] == pd.Timestamp("2022-11-28T00:00:00.000Z")
|
||||
assert candle5["date_1M"] == pd.Timestamp("2022-10-01T00:00:00.000Z")
|
||||
|
||||
|
||||
def test_merge_informative_pair_no_overlap():
|
||||
# Covers roughly a day
|
||||
data = generate_test_data("1m", 1440, "2022-11-28")
|
||||
# Data stops WAY before the main data starts
|
||||
informative = generate_test_data("1h", 40, "2022-11-01")
|
||||
|
||||
result = merge_informative_pair(data, informative, "1m", "1h", ffill=True)
|
||||
|
||||
assert isinstance(result, pd.DataFrame)
|
||||
assert len(result) == len(data)
|
||||
assert "date" in result.columns
|
||||
assert result["date"].equals(data["date"])
|
||||
assert "date_1h" in result.columns
|
||||
# If there's no overlap, forward filling should not fill anything
|
||||
assert result["date_1h"].isnull().all()
|
||||
|
||||
|
||||
def test_merge_informative_pair_same():
|
||||
data = generate_test_data("15m", 40)
|
||||
|
||||
@@ -96,6 +96,16 @@ def test_load_strategy_invalid_directory(caplog, default_conf, tmp_path):
|
||||
assert log_has_re(r"Path .*" + r"some.*path.*" + r".* does not exist", caplog)
|
||||
|
||||
|
||||
def test_load_strategy_skip_other_files(caplog, default_conf, tmp_path):
|
||||
default_conf["user_data_dir"] = tmp_path
|
||||
caplog.set_level(logging.DEBUG)
|
||||
|
||||
s = StrategyResolver._load_strategy("StrategyTestV3", config=default_conf)
|
||||
assert isinstance(s, IStrategy)
|
||||
|
||||
assert log_has_re(r"Skipping .* as it does not contain class StrategyTestV3\.", caplog)
|
||||
|
||||
|
||||
def test_load_not_found_strategy(default_conf, tmp_path):
|
||||
default_conf["user_data_dir"] = tmp_path
|
||||
default_conf["strategy"] = "NotFoundStrategy"
|
||||
|
||||
@@ -64,7 +64,6 @@ def test_hyperopt_real_parameter():
|
||||
|
||||
def test_hyperopt_decimal_parameter():
|
||||
HyperoptStateContainer.set_state(HyperoptState.INDICATORS)
|
||||
# TODO: Check for get_space??
|
||||
from freqtrade.optimize.space import SKDecimal
|
||||
|
||||
with pytest.raises(OperationalException, match=r"DecimalParameter space must be.*"):
|
||||
|
||||
+4
-4
@@ -168,12 +168,12 @@ def test_plural() -> None:
|
||||
"conn_url,expected",
|
||||
[
|
||||
(
|
||||
"postgresql+psycopg2://scott123:scott123@host:1245/dbname",
|
||||
"postgresql+psycopg2://scott123:*****@host:1245/dbname",
|
||||
"postgresql+psycopg://scott123:scott123@host:1245/dbname",
|
||||
"postgresql+psycopg://scott123:*****@host:1245/dbname",
|
||||
),
|
||||
(
|
||||
"postgresql+psycopg2://scott123:scott123@host.name.com/dbname",
|
||||
"postgresql+psycopg2://scott123:*****@host.name.com/dbname",
|
||||
"postgresql+psycopg://scott123:scott123@host.name.com/dbname",
|
||||
"postgresql+psycopg://scott123:*****@host.name.com/dbname",
|
||||
),
|
||||
(
|
||||
"mariadb+mariadbconnector://app_user:Password123!@127.0.0.1:3306/company",
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
"""
|
||||
Run pip audit to check for known security vulnerabilities in installed packages.
|
||||
Original Idea and base for this implementation by Michael Kennedy's blog:
|
||||
https://mkennedy.codes/posts/python-supply-chain-security-made-easy/
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
IN_GITHUB_ACTIONS = os.getenv("GITHUB_ACTIONS") == "true"
|
||||
|
||||
|
||||
# Skip this test in github actions - github issues a security warning on it's own.
|
||||
# This is to detect local transient dependencies.
|
||||
@pytest.mark.skipif(IN_GITHUB_ACTIONS, reason="Skip pip-audit in GitHub Actions")
|
||||
def test_pip_audit_no_vulnerabilities():
|
||||
"""
|
||||
Run pip-audit to check for known security vulnerabilities.
|
||||
|
||||
This test will fail if any vulnerabilities are detected in the installed packages.
|
||||
|
||||
Note: CVE-2025-53000 (nbconvert Windows vulnerability) is ignored as it only affects
|
||||
Windows platforms and is a known acceptable risk for this project.
|
||||
"""
|
||||
# Get the project root directory
|
||||
project_root = Path(__file__).parent.parent
|
||||
command = [
|
||||
sys.executable,
|
||||
"-m",
|
||||
"pip_audit",
|
||||
# "--format=json",
|
||||
"--progress-spinner=off",
|
||||
"--ignore-vuln",
|
||||
"CVE-2025-53000",
|
||||
"--skip-editable",
|
||||
]
|
||||
|
||||
# Run pip-audit with JSON output for easier parsing
|
||||
try:
|
||||
result = subprocess.run(
|
||||
command,
|
||||
cwd=project_root,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=120, # 2 minute timeout
|
||||
)
|
||||
except subprocess.TimeoutExpired:
|
||||
pytest.fail("pip-audit command timed out after 120 seconds")
|
||||
except FileNotFoundError:
|
||||
pytest.fail("pip-audit not installed or not accessible")
|
||||
|
||||
# Check if pip-audit found any vulnerabilities
|
||||
if result.returncode != 0:
|
||||
# pip-audit returns non-zero when vulnerabilities are found
|
||||
error_output = result.stdout + "\n" + result.stderr
|
||||
|
||||
# Check if it's an actual vulnerability vs an error
|
||||
if "vulnerabilities found" in error_output.lower() or '"dependencies"' in result.stdout:
|
||||
pytest.fail(
|
||||
f"pip-audit detected security vulnerabilities!\n\n"
|
||||
f"Output:\n{result.stdout}\n\n"
|
||||
f"Please review and update vulnerable packages.\n"
|
||||
f"Run manually with: {' '.join(command)}"
|
||||
)
|
||||
else:
|
||||
# Some other error occurred
|
||||
pytest.fail(
|
||||
f"pip-audit failed to run properly:\n\nReturn code: {result.returncode}\n"
|
||||
f"Output: {error_output}\n"
|
||||
)
|
||||
|
||||
# Success - no vulnerabilities found
|
||||
assert result.returncode == 0, "pip-audit should return 0 when no vulnerabilities are found"
|
||||
|
||||
|
||||
def test_pip_audit_runs_successfully():
|
||||
"""
|
||||
Verify that pip-audit can run successfully (even if vulnerabilities are found).
|
||||
|
||||
This is a smoke test to ensure pip-audit is properly installed and functional.
|
||||
"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-m", "pip_audit", "--version"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
)
|
||||
assert result.returncode == 0, f"pip-audit --version failed: {result.stderr}"
|
||||
assert "pip-audit" in result.stdout.lower(), "pip-audit version output unexpected"
|
||||
except FileNotFoundError:
|
||||
pytest.fail("pip-audit not installed")
|
||||
except subprocess.TimeoutExpired:
|
||||
pytest.fail("pip-audit --version timed out")
|
||||
@@ -15,7 +15,7 @@ def test_strategy_updater_start(user_dir, capsys) -> None:
|
||||
tmpdirp = Path(user_dir) / "strategies"
|
||||
tmpdirp.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy(teststrats / "strategy_test_v2.py", tmpdirp)
|
||||
old_code = (teststrats / "strategy_test_v2.py").read_text()
|
||||
old_code = (teststrats / "strategy_test_v2.py").read_text(encoding="utf-8")
|
||||
|
||||
args = ["strategy-updater", "--userdir", str(user_dir), "--strategy-list", "StrategyTestV2"]
|
||||
pargs = get_args(args)
|
||||
@@ -29,7 +29,7 @@ def test_strategy_updater_start(user_dir, capsys) -> None:
|
||||
# updated file exists
|
||||
new_file = tmpdirp / "strategy_test_v2.py"
|
||||
assert new_file.exists()
|
||||
new_code = new_file.read_text()
|
||||
new_code = new_file.read_text(encoding="utf-8")
|
||||
assert "INTERFACE_VERSION = 3" in new_code
|
||||
assert "INTERFACE_VERSION = 2" in old_code
|
||||
captured = capsys.readouterr()
|
||||
|
||||
@@ -191,6 +191,8 @@ def test_get_trade_stake_amount_unlimited_amount(
|
||||
(1000, None, 1000, 10000, None, 1000), # No min-stake-amount could be determined
|
||||
# Rebuy - resulting in too high stake amount. Adjusting.
|
||||
(2000, 15, 2000, 3000, 1500, 1500),
|
||||
("undefined", 15, 100, 10000, None, 0), # string stake amount
|
||||
("22.2", 15, 100, 10000, None, 0), # string stake amount but as float
|
||||
],
|
||||
)
|
||||
def test_validate_stake_amount(
|
||||
|
||||
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Binary file not shown.
-1
File diff suppressed because one or more lines are too long
@@ -1,65 +0,0 @@
|
||||
import shutil
|
||||
|
||||
import pytest
|
||||
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.util.migrations import migrate_binance_futures_data, migrate_data
|
||||
from freqtrade.util.migrations.binance_mig import migrate_binance_futures_names
|
||||
from tests.conftest import create_mock_trades_usdt, log_has
|
||||
|
||||
|
||||
def test_binance_mig_data_conversion(default_conf_usdt, tmp_path, testdatadir):
|
||||
# call doing nothing (spot mode)
|
||||
migrate_binance_futures_data(default_conf_usdt)
|
||||
default_conf_usdt["trading_mode"] = "futures"
|
||||
pair_old = "XRP_USDT"
|
||||
pair_unified = "XRP_USDT_USDT"
|
||||
futures_src = testdatadir / "futures"
|
||||
futures_dst = tmp_path / "futures"
|
||||
futures_dst.mkdir()
|
||||
files = [
|
||||
"-1h-mark.feather",
|
||||
"-1h-futures.feather",
|
||||
"-8h-funding_rate.feather",
|
||||
"-8h-mark.feather",
|
||||
]
|
||||
|
||||
# Copy files to tmpdir and rename to old naming
|
||||
for file in files:
|
||||
fn_after = futures_dst / f"{pair_old}{file}"
|
||||
shutil.copy(futures_src / f"{pair_unified}{file}", fn_after)
|
||||
|
||||
default_conf_usdt["datadir"] = tmp_path
|
||||
# Migrate files to unified namings
|
||||
migrate_binance_futures_data(default_conf_usdt)
|
||||
|
||||
for file in files:
|
||||
fn_after = futures_dst / f"{pair_unified}{file}"
|
||||
assert fn_after.exists()
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_persistence")
|
||||
def test_binance_mig_db_conversion(default_conf_usdt, fee, caplog):
|
||||
# Does nothing in spot mode
|
||||
migrate_binance_futures_names(default_conf_usdt)
|
||||
|
||||
create_mock_trades_usdt(fee, None)
|
||||
|
||||
for t in Trade.get_trades():
|
||||
t.trading_mode = "FUTURES"
|
||||
t.exchange = "binance"
|
||||
Trade.commit()
|
||||
|
||||
default_conf_usdt["trading_mode"] = "futures"
|
||||
migrate_binance_futures_names(default_conf_usdt)
|
||||
assert log_has("Migrating binance futures pairs in database.", caplog)
|
||||
|
||||
|
||||
def test_migration_wrapper(default_conf_usdt, mocker):
|
||||
default_conf_usdt["trading_mode"] = "futures"
|
||||
binmock = mocker.patch("freqtrade.util.migrations.migrate_binance_futures_data")
|
||||
funding_mock = mocker.patch("freqtrade.util.migrations.migrate_funding_fee_timeframe")
|
||||
migrate_data(default_conf_usdt)
|
||||
|
||||
assert binmock.call_count == 1
|
||||
assert funding_mock.call_count == 1
|
||||
@@ -82,7 +82,7 @@ def test_dt_humanize() -> None:
|
||||
assert dt_humanize_delta(dt_now() - timedelta(minutes=50)) == "50 minutes ago"
|
||||
assert dt_humanize_delta(dt_now() - timedelta(hours=16)) == "16 hours ago"
|
||||
assert dt_humanize_delta(dt_now() - timedelta(hours=16, minutes=30)) == "16 hours ago"
|
||||
assert dt_humanize_delta(dt_now() - timedelta(days=16, hours=10, minutes=25)) == "16 days ago"
|
||||
assert dt_humanize_delta(dt_now() - timedelta(days=16, hours=10, minutes=25)) == "a month ago"
|
||||
assert dt_humanize_delta(dt_now() - timedelta(minutes=50)) == "50 minutes ago"
|
||||
|
||||
|
||||
@@ -108,6 +108,7 @@ def test_format_date() -> None:
|
||||
date = datetime(2021, 9, 30, 22, 59, 3, 455555, tzinfo=UTC)
|
||||
assert format_date(date) == "2021-09-30 22:59:03"
|
||||
assert format_date(None) == ""
|
||||
assert format_date(None, "closed") == "closed"
|
||||
|
||||
|
||||
def test_format_ms_time_detailed() -> None:
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
from datetime import timedelta
|
||||
|
||||
from freqtrade.util import decimals_per_coin, fmt_coin, fmt_coin2, format_duration, round_value
|
||||
from freqtrade.util import (
|
||||
decimals_per_coin,
|
||||
fmt_coin,
|
||||
fmt_coin2,
|
||||
format_duration,
|
||||
format_pct,
|
||||
round_value,
|
||||
)
|
||||
|
||||
|
||||
def test_decimals_per_coin():
|
||||
@@ -25,6 +32,7 @@ def test_fmt_coin():
|
||||
assert fmt_coin(0.1274512123, "BTC", False) == "0.12745121"
|
||||
assert fmt_coin(0.1274512123, "ETH", False) == "0.12745"
|
||||
assert fmt_coin(222.2, "USDT", False, True) == "222.200"
|
||||
assert fmt_coin(float("nan"), "USDT", False, True) == "N/A"
|
||||
|
||||
|
||||
def test_fmt_coin2():
|
||||
@@ -35,6 +43,7 @@ def test_fmt_coin2():
|
||||
assert fmt_coin2(0.1274512123, "BTC") == "0.12745121 BTC"
|
||||
assert fmt_coin2(0.1274512123, "ETH") == "0.12745121 ETH"
|
||||
assert fmt_coin2(0.00001245, "PEPE") == "0.00001245 PEPE"
|
||||
assert fmt_coin2(float("nan"), "PEPE") == "N/A PEPE"
|
||||
|
||||
|
||||
def test_round_value():
|
||||
@@ -46,6 +55,10 @@ def test_round_value():
|
||||
assert round_value(0.1274512123, 5) == "0.12745"
|
||||
assert round_value(222.2, 3, True) == "222.200"
|
||||
assert round_value(222.2, 0, True) == "222"
|
||||
assert round_value(float("nan"), 0, True) == "N/A"
|
||||
assert round_value(float("nan"), 10, True) == "N/A"
|
||||
assert round_value(None, 10, True) == "N/A"
|
||||
assert round_value(None, 1, True) == "N/A"
|
||||
|
||||
|
||||
def test_format_duration():
|
||||
@@ -55,3 +68,13 @@ def test_format_duration():
|
||||
assert format_duration(timedelta(minutes=1445)) == "1d 00:05"
|
||||
assert format_duration(timedelta(minutes=11445)) == "7d 22:45"
|
||||
assert format_duration(timedelta(minutes=101445)) == "70d 10:45"
|
||||
|
||||
|
||||
def test_format_pct():
|
||||
assert format_pct(0.1234) == "12.34%"
|
||||
assert format_pct(0.1) == "10.00%"
|
||||
assert format_pct(0.0) == "0.00%"
|
||||
assert format_pct(-0.0567) == "-5.67%"
|
||||
assert format_pct(-1.5567) == "-155.67%"
|
||||
assert format_pct(None) == "N/A"
|
||||
assert format_pct(float("nan")) == "N/A"
|
||||
|
||||
@@ -5,13 +5,13 @@ from freqtrade.util.migrations import migrate_funding_fee_timeframe
|
||||
|
||||
def test_migrate_funding_rate_timeframe(default_conf_usdt, tmp_path, testdatadir):
|
||||
copytree(testdatadir / "futures", tmp_path / "futures")
|
||||
file_4h = tmp_path / "futures" / "XRP_USDT_USDT-4h-funding_rate.feather"
|
||||
file_8h = tmp_path / "futures" / "XRP_USDT_USDT-8h-funding_rate.feather"
|
||||
file_30m = tmp_path / "futures" / "XRP_USDT_USDT-30m-funding_rate.feather"
|
||||
file_1h_fr = tmp_path / "futures" / "XRP_USDT_USDT-1h-funding_rate.feather"
|
||||
file_1h = tmp_path / "futures" / "XRP_USDT_USDT-1h-futures.feather"
|
||||
file_8h.rename(file_4h)
|
||||
file_1h_fr.rename(file_30m)
|
||||
assert file_1h.exists()
|
||||
assert file_4h.exists()
|
||||
assert not file_8h.exists()
|
||||
assert file_30m.exists()
|
||||
assert not file_1h_fr.exists()
|
||||
|
||||
default_conf_usdt["datadir"] = tmp_path
|
||||
|
||||
@@ -22,7 +22,7 @@ def test_migrate_funding_rate_timeframe(default_conf_usdt, tmp_path, testdatadir
|
||||
|
||||
migrate_funding_fee_timeframe(default_conf_usdt, None)
|
||||
|
||||
assert not file_4h.exists()
|
||||
assert file_8h.exists()
|
||||
assert not file_30m.exists()
|
||||
assert file_1h_fr.exists()
|
||||
# futures files is untouched.
|
||||
assert file_1h.exists()
|
||||
|
||||
Reference in New Issue
Block a user