Merge branch 'develop' into pr/x-mass/12315

This commit is contained in:
Matthias
2026-02-01 16:51:01 +01:00
259 changed files with 27176 additions and 16968 deletions
+55 -7
View File
@@ -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",
+2 -2
View File
@@ -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
View File
@@ -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,
+2
View File
@@ -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,
+14 -5
View File
@@ -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(
+1 -9
View File
@@ -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)
+6 -3
View File
@@ -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)
+41 -1
View File
@@ -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
View File
@@ -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
+14 -1
View File
@@ -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(
+118 -4
View File
@@ -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)
+44 -3
View File
@@ -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
View File
@@ -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)
+41
View File
@@ -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"}
+8 -10
View File
@@ -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(
+2 -1
View File
@@ -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
+523 -31
View File
@@ -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
+8 -8
View File
@@ -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):
+20 -14
View File
@@ -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()
+65 -22
View File
@@ -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"]
+1 -9
View File
@@ -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"),
],
)
+27 -14
View File
@@ -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)
+18 -4
View File
@@ -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"
+80 -48
View File
@@ -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
+43 -43
View File
@@ -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
+13 -10
View File
@@ -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']
+55 -43
View File
@@ -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:
+2 -2
View File
@@ -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)
+19
View File
@@ -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")
+8 -8
View File
@@ -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
+2 -1
View File
@@ -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",
+14 -10
View File
@@ -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,
}
+34 -2
View File
@@ -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(
+2 -2
View File
@@ -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)
+8
View File
@@ -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
View File
@@ -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
View File
@@ -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
+21 -31
View File
@@ -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(
{
+14 -32
View File
@@ -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
+10
View File
@@ -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
+10
View File
@@ -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
+45 -14
View File
@@ -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)}"
+27 -2
View File
@@ -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)
+10
View File
@@ -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
View File
@@ -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",
+99
View File
@@ -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")
+2 -2
View File
@@ -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()
+2
View File
@@ -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(
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
-65
View File
@@ -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
+2 -1
View File
@@ -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:
+24 -1
View File
@@ -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"
+7 -7
View File
@@ -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()