Merge branch 'develop' into maint/python_3.14
This commit is contained in:
@@ -198,6 +198,8 @@ def test_list_timeframes(mocker, capsys):
|
||||
"1h": "hour",
|
||||
"1d": "day",
|
||||
}
|
||||
api_mock.options = {}
|
||||
|
||||
patch_exchange(mocker, api_mock=api_mock, exchange="bybit")
|
||||
args = [
|
||||
"list-timeframes",
|
||||
@@ -286,6 +288,52 @@ def test_list_timeframes(mocker, capsys):
|
||||
assert re.search(r"^1h$", captured.out, re.MULTILINE)
|
||||
assert re.search(r"^1d$", captured.out, re.MULTILINE)
|
||||
|
||||
api_mock.options = {
|
||||
"timeframes": {
|
||||
"spot": {"1m": "1m", "5m": "5m", "15m": "15m"},
|
||||
"swap": {"1m": "1m", "15m": "15m", "1h": "1h"},
|
||||
}
|
||||
}
|
||||
|
||||
args = [
|
||||
"list-timeframes",
|
||||
"--exchange",
|
||||
"binance",
|
||||
]
|
||||
start_list_timeframes(get_args(args))
|
||||
captured = capsys.readouterr()
|
||||
assert re.match(
|
||||
"Timeframes available for the exchange `Binance`: 1m, 5m, 15m",
|
||||
captured.out,
|
||||
)
|
||||
|
||||
args = [
|
||||
"list-timeframes",
|
||||
"--exchange",
|
||||
"binance",
|
||||
"--trading-mode",
|
||||
"spot",
|
||||
]
|
||||
start_list_timeframes(get_args(args))
|
||||
captured = capsys.readouterr()
|
||||
assert re.match(
|
||||
"Timeframes available for the exchange `Binance`: 1m, 5m, 15m",
|
||||
captured.out,
|
||||
)
|
||||
args = [
|
||||
"list-timeframes",
|
||||
"--exchange",
|
||||
"binance",
|
||||
"--trading-mode",
|
||||
"futures",
|
||||
]
|
||||
start_list_timeframes(get_args(args))
|
||||
captured = capsys.readouterr()
|
||||
assert re.match(
|
||||
"Timeframes available for the exchange `Binance`: 1m, 15m, 1h",
|
||||
captured.out,
|
||||
)
|
||||
|
||||
|
||||
def test_list_markets(mocker, markets_static, capsys):
|
||||
api_mock = MagicMock()
|
||||
@@ -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",
|
||||
|
||||
@@ -303,6 +303,7 @@ def mock_order_usdt_6(is_short: bool):
|
||||
"side": entry_side(is_short),
|
||||
"type": "limit",
|
||||
"price": 10.0,
|
||||
"cost": 20.0,
|
||||
"amount": 2.0,
|
||||
"filled": 2.0,
|
||||
"remaining": 0.0,
|
||||
@@ -317,6 +318,7 @@ def mock_order_usdt_6_exit(is_short: bool):
|
||||
"side": exit_side(is_short),
|
||||
"type": "limit",
|
||||
"price": 12.0,
|
||||
"cost": 24.0,
|
||||
"amount": 2.0,
|
||||
"filled": 0.0,
|
||||
"remaining": 2.0,
|
||||
|
||||
@@ -290,20 +290,23 @@ def test_combine_dataframes_with_mean(testdatadir):
|
||||
|
||||
|
||||
def test_combined_dataframes_with_rel_mean(testdatadir):
|
||||
pairs = ["ETH/BTC", "ADA/BTC"]
|
||||
pairs = ["BTC/USDT", "XRP/USDT"]
|
||||
data = load_data(datadir=testdatadir, pairs=pairs, timeframe="5m")
|
||||
df = combined_dataframes_with_rel_mean(
|
||||
data, datetime(2018, 1, 12, tzinfo=UTC), datetime(2018, 1, 28, tzinfo=UTC)
|
||||
data,
|
||||
fromdt=data["BTC/USDT"].at[0, "date"],
|
||||
todt=data["BTC/USDT"].at[data["BTC/USDT"].index[-1], "date"],
|
||||
)
|
||||
assert isinstance(df, DataFrame)
|
||||
assert "ETH/BTC" not in df.columns
|
||||
assert "ADA/BTC" not in df.columns
|
||||
assert "BTC/USDT" not in df.columns
|
||||
assert "XRP/USDT" not in df.columns
|
||||
assert "mean" in df.columns
|
||||
assert "rel_mean" in df.columns
|
||||
assert "count" in df.columns
|
||||
assert df.iloc[0]["count"] == 2
|
||||
assert df.iloc[-1]["count"] == 2
|
||||
assert len(df) < len(data["ETH/BTC"])
|
||||
assert len(df) < len(data["BTC/USDT"])
|
||||
assert df["rel_mean"].between(-0.5, 0.5).all()
|
||||
|
||||
|
||||
def test_combine_dataframes_with_mean_no_data(testdatadir):
|
||||
@@ -575,12 +578,18 @@ def test_calculate_max_drawdown2():
|
||||
# No losing trade ...
|
||||
drawdown = calculate_max_drawdown(df, date_col="open_date", value_col="profit")
|
||||
assert drawdown.drawdown_abs == 0.0
|
||||
assert drawdown.low_value == 0.0
|
||||
assert drawdown.current_high_value >= 0.0
|
||||
assert drawdown.current_drawdown_abs == 0.0
|
||||
|
||||
df1 = DataFrame(zip(values[:5], dates[:5], strict=False), columns=["profit", "open_date"])
|
||||
df1.loc[:, "profit"] = df1["profit"] * -1
|
||||
# No winning trade ...
|
||||
drawdown = calculate_max_drawdown(df1, date_col="open_date", value_col="profit")
|
||||
assert drawdown.drawdown_abs == 0.055545
|
||||
assert drawdown.high_value == 0.0
|
||||
assert drawdown.current_high_value == 0.0
|
||||
assert drawdown.current_drawdown_abs == 0.055545
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -9,7 +9,7 @@ from freqtrade.enums import CandleType, RunMode
|
||||
from freqtrade.exceptions import ExchangeError, OperationalException
|
||||
from freqtrade.plugins.pairlistmanager import PairListManager
|
||||
from freqtrade.util import dt_utc
|
||||
from tests.conftest import EXMS, generate_test_data, get_patched_exchange
|
||||
from tests.conftest import EXMS, generate_test_data, get_patched_exchange, log_has_re
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -185,6 +185,28 @@ def test_get_pair_dataframe(mocker, default_conf, ohlcv_history, candle_type):
|
||||
assert len(df) == 2 # ohlcv_history is limited to 2 rows now
|
||||
|
||||
|
||||
def test_get_pair_dataframe_funding_rate(mocker, default_conf, ohlcv_history, caplog):
|
||||
default_conf["runmode"] = RunMode.DRY_RUN
|
||||
timeframe = "1h"
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
candletype = CandleType.FUNDING_RATE
|
||||
exchange._klines[("XRP/BTC", timeframe, candletype)] = ohlcv_history
|
||||
exchange._klines[("UNITTEST/BTC", timeframe, candletype)] = ohlcv_history
|
||||
|
||||
dp = DataProvider(default_conf, exchange)
|
||||
assert dp.runmode == RunMode.DRY_RUN
|
||||
assert ohlcv_history.equals(
|
||||
dp.get_pair_dataframe("UNITTEST/BTC", timeframe, candle_type="funding_rate")
|
||||
)
|
||||
msg = r".*funding rate timeframe not matching"
|
||||
assert not log_has_re(msg, caplog)
|
||||
|
||||
assert ohlcv_history.equals(
|
||||
dp.get_pair_dataframe("UNITTEST/BTC", "5h", candle_type="funding_rate")
|
||||
)
|
||||
assert log_has_re(msg, caplog)
|
||||
|
||||
|
||||
def test_available_pairs(mocker, default_conf, ohlcv_history):
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
timeframe = default_conf["timeframe"]
|
||||
@@ -636,3 +658,21 @@ def test_check_delisting(mocker, default_conf_usdt):
|
||||
assert res == dt_utc(2025, 10, 2)
|
||||
|
||||
assert delist_mock2.call_count == 1
|
||||
|
||||
|
||||
def test_get_funding_rate_timeframe(mocker, default_conf_usdt):
|
||||
default_conf_usdt["trading_mode"] = "futures"
|
||||
default_conf_usdt["margin_mode"] = "isolated"
|
||||
exchange = get_patched_exchange(mocker, default_conf_usdt)
|
||||
mock_get_option = mocker.spy(exchange, "get_option")
|
||||
dp = DataProvider(default_conf_usdt, exchange)
|
||||
|
||||
assert dp.get_funding_rate_timeframe() == "1h"
|
||||
mock_get_option.assert_called_once_with("funding_fee_timeframe")
|
||||
|
||||
|
||||
def test_get_funding_rate_timeframe_no_exchange(default_conf_usdt):
|
||||
dp = DataProvider(default_conf_usdt, None)
|
||||
|
||||
with pytest.raises(OperationalException, match=r"Exchange is not available to DataProvider."):
|
||||
dp.get_funding_rate_timeframe()
|
||||
|
||||
+49
-11
@@ -534,18 +534,19 @@ def test_validate_backtest_data(default_conf, mocker, caplog, testdatadir) -> No
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"trademode,callcount",
|
||||
"trademode,callcount, callcount_parallel",
|
||||
[
|
||||
("spot", 4),
|
||||
("margin", 4),
|
||||
("futures", 8), # Called 8 times - 4 normal, 2 funding and 2 mark/index calls
|
||||
("spot", 4, 2),
|
||||
("margin", 4, 2),
|
||||
("futures", 8, 4), # Called 8 times - 4 normal, 2 funding and 2 mark/index calls
|
||||
],
|
||||
)
|
||||
def test_refresh_backtest_ohlcv_data(
|
||||
mocker, default_conf, markets, caplog, testdatadir, trademode, callcount
|
||||
mocker, default_conf, markets, caplog, testdatadir, trademode, callcount, callcount_parallel
|
||||
):
|
||||
caplog.set_level(logging.DEBUG)
|
||||
dl_mock = mocker.patch("freqtrade.data.history.history_utils._download_pair_history")
|
||||
mocker.patch(f"{EXMS}.verify_candle_type_support", MagicMock())
|
||||
|
||||
def parallel_mock(pairs, timeframe, candle_type, **kwargs):
|
||||
return {(pair, timeframe, candle_type): DataFrame() for pair in pairs}
|
||||
@@ -573,14 +574,50 @@ def test_refresh_backtest_ohlcv_data(
|
||||
)
|
||||
|
||||
# Called once per timeframe (as we return an empty dataframe)
|
||||
assert parallel_mock.call_count == 2
|
||||
# called twice for spot/margin and 4 times for futures
|
||||
assert parallel_mock.call_count == callcount_parallel
|
||||
assert dl_mock.call_count == callcount
|
||||
assert dl_mock.call_args[1]["timerange"].starttype == "date"
|
||||
|
||||
assert log_has_re(r"Downloading pair ETH/BTC, .* interval 1m\.", caplog)
|
||||
if trademode == "futures":
|
||||
assert log_has_re(r"Downloading pair ETH/BTC, funding_rate, interval 8h\.", caplog)
|
||||
assert log_has_re(r"Downloading pair ETH/BTC, mark, interval 4h\.", caplog)
|
||||
assert log_has_re(r"Downloading pair ETH/BTC, funding_rate, interval 1h\.", caplog)
|
||||
assert log_has_re(r"Downloading pair ETH/BTC, mark, interval 1h\.", caplog)
|
||||
|
||||
# Test with only one pair - no parallel download should happen 1 pair/timeframe combination
|
||||
# doesn't justify parallelization
|
||||
parallel_mock.reset_mock()
|
||||
dl_mock.reset_mock()
|
||||
refresh_backtest_ohlcv_data(
|
||||
exchange=ex,
|
||||
pairs=[
|
||||
"ETH/BTC",
|
||||
],
|
||||
timeframes=["5m"],
|
||||
datadir=testdatadir,
|
||||
timerange=timerange,
|
||||
erase=False,
|
||||
trading_mode=trademode,
|
||||
)
|
||||
assert parallel_mock.call_count == 0
|
||||
|
||||
if trademode == "futures":
|
||||
dl_mock.reset_mock()
|
||||
refresh_backtest_ohlcv_data(
|
||||
exchange=ex,
|
||||
pairs=[
|
||||
"ETH/BTC",
|
||||
],
|
||||
timeframes=["5m", "1h"],
|
||||
datadir=testdatadir,
|
||||
timerange=timerange,
|
||||
erase=False,
|
||||
trading_mode=trademode,
|
||||
no_parallel_download=True,
|
||||
candle_types=["premiumIndex", "funding_rate"],
|
||||
)
|
||||
assert parallel_mock.call_count == 0
|
||||
assert dl_mock.call_count == 3 # 2 timeframes premiumIndex + 1x funding_rate
|
||||
|
||||
|
||||
def test_download_data_no_markets(mocker, default_conf, caplog, testdatadir):
|
||||
@@ -780,6 +817,7 @@ def test_download_all_pairs_history_parallel(mocker, default_conf_usdt):
|
||||
exchange.refresh_latest_ohlcv.reset_mock()
|
||||
|
||||
# Test without timerange
|
||||
# expected to call refresh_latest_ohlcv - as we can't know how much will be required.
|
||||
result3 = _download_all_pairs_history_parallel(
|
||||
exchange=exchange,
|
||||
pairs=pairs,
|
||||
@@ -787,8 +825,8 @@ def test_download_all_pairs_history_parallel(mocker, default_conf_usdt):
|
||||
candle_type=candle_type,
|
||||
timerange=None,
|
||||
)
|
||||
assert result3 == {}
|
||||
assert exchange.refresh_latest_ohlcv.call_count == 0
|
||||
assert result3 == expected
|
||||
assert exchange.refresh_latest_ohlcv.call_count == 1
|
||||
|
||||
|
||||
def test_download_pair_history_with_pair_candles(mocker, default_conf, tmp_path, caplog) -> None:
|
||||
@@ -878,7 +916,7 @@ def test_download_pair_history_with_pair_candles(mocker, default_conf, tmp_path,
|
||||
assert get_historic_ohlcv_mock.call_count == 0
|
||||
|
||||
# Verify the log message indicating parallel method was used (line 315-316)
|
||||
assert log_has("Downloaded data for TEST/BTC with length 3. Parallel Method.", caplog)
|
||||
assert log_has("Downloaded data for TEST/BTC, 5m, spot with length 3. Parallel Method.", caplog)
|
||||
|
||||
# Verify data was stored
|
||||
assert data_handler_mock.ohlcv_store.call_count == 1
|
||||
|
||||
@@ -157,7 +157,8 @@ def test_create_stoploss_order_dry_run_binance(default_conf, mocker):
|
||||
assert "type" in order
|
||||
|
||||
assert order["type"] == order_type
|
||||
assert order["price"] == 220
|
||||
assert order["price"] == 217.8
|
||||
assert order["stopPrice"] == 220
|
||||
assert order["amount"] == 1
|
||||
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
from copy import deepcopy
|
||||
from datetime import timedelta
|
||||
from unittest.mock import MagicMock, PropertyMock
|
||||
|
||||
import pytest
|
||||
|
||||
from freqtrade.enums import CandleType, MarginMode, TradingMode
|
||||
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
|
||||
|
||||
@@ -193,3 +194,43 @@ def test__lev_prep_bitget(default_conf, mocker):
|
||||
assert api_mock.set_margin_mode.call_count == 0
|
||||
assert api_mock.set_leverage.call_count == 1
|
||||
api_mock.set_leverage.assert_called_with(symbol="BTC/USDC:USDC", leverage=19.99)
|
||||
|
||||
|
||||
def test_check_delisting_time_bitget(default_conf_usdt, mocker):
|
||||
exchange = get_patched_exchange(mocker, default_conf_usdt, exchange="bitget")
|
||||
exchange._config["runmode"] = RunMode.BACKTEST
|
||||
delist_fut_mock = MagicMock(return_value=None)
|
||||
mocker.patch.object(exchange, "_check_delisting_futures", delist_fut_mock)
|
||||
|
||||
# Invalid run mode
|
||||
resp = exchange.check_delisting_time("BTC/USDT")
|
||||
assert resp is None
|
||||
assert delist_fut_mock.call_count == 0
|
||||
|
||||
# Delist spot called
|
||||
exchange._config["runmode"] = RunMode.DRY_RUN
|
||||
resp1 = exchange.check_delisting_time("BTC/USDT")
|
||||
assert resp1 is None
|
||||
assert delist_fut_mock.call_count == 0
|
||||
|
||||
# Delist futures called
|
||||
exchange.trading_mode = TradingMode.FUTURES
|
||||
resp1 = exchange.check_delisting_time("BTC/USDT:USDT")
|
||||
assert resp1 is None
|
||||
assert delist_fut_mock.call_count == 1
|
||||
|
||||
|
||||
def test__check_delisting_futures_bitget(default_conf_usdt, mocker, markets):
|
||||
markets["BTC/USDT:USDT"] = deepcopy(markets["SOL/BUSD:BUSD"])
|
||||
markets["BTC/USDT:USDT"]["info"]["limitOpenTime"] = "-1"
|
||||
markets["SOL/BUSD:BUSD"]["info"]["limitOpenTime"] = "-1"
|
||||
markets["ADA/USDT:USDT"]["info"]["limitOpenTime"] = "1760745600000" # 2025-10-18
|
||||
exchange = get_patched_exchange(mocker, default_conf_usdt, exchange="bitget")
|
||||
mocker.patch(f"{EXMS}.markets", PropertyMock(return_value=markets))
|
||||
|
||||
resp_sol = exchange._check_delisting_futures("SOL/BUSD:BUSD")
|
||||
# No delisting date
|
||||
assert resp_sol is None
|
||||
# Has a delisting date
|
||||
resp_ada = exchange._check_delisting_futures("ADA/USDT:USDT")
|
||||
assert resp_ada == dt_utc(2025, 10, 18)
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
from copy import deepcopy
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from unittest.mock import MagicMock
|
||||
from unittest.mock import MagicMock, PropertyMock
|
||||
|
||||
import pytest
|
||||
|
||||
from freqtrade.enums.marginmode import MarginMode
|
||||
from freqtrade.enums.tradingmode import TradingMode
|
||||
from freqtrade.enums import MarginMode, RunMode, TradingMode
|
||||
from freqtrade.util import dt_utc
|
||||
from tests.conftest import EXMS, get_mock_coro, get_patched_exchange, log_has
|
||||
from tests.exchange.test_exchange import ccxt_exceptionhandlers
|
||||
|
||||
@@ -214,3 +215,43 @@ def test_bybit__order_needs_price(
|
||||
exchange.unified_account = uta
|
||||
|
||||
assert exchange._order_needs_price(side, order_type) == expected
|
||||
|
||||
|
||||
def test_check_delisting_time_bybit(default_conf_usdt, mocker):
|
||||
exchange = get_patched_exchange(mocker, default_conf_usdt, exchange="bybit")
|
||||
exchange._config["runmode"] = RunMode.BACKTEST
|
||||
delist_fut_mock = MagicMock(return_value=None)
|
||||
mocker.patch.object(exchange, "_check_delisting_futures", delist_fut_mock)
|
||||
|
||||
# Invalid run mode
|
||||
resp = exchange.check_delisting_time("BTC/USDT:USDT")
|
||||
assert resp is None
|
||||
assert delist_fut_mock.call_count == 0
|
||||
|
||||
# Delist spot called
|
||||
exchange._config["runmode"] = RunMode.DRY_RUN
|
||||
resp1 = exchange.check_delisting_time("BTC/USDT")
|
||||
assert resp1 is None
|
||||
assert delist_fut_mock.call_count == 0
|
||||
|
||||
# Delist futures called
|
||||
exchange.trading_mode = TradingMode.FUTURES
|
||||
resp1 = exchange.check_delisting_time("BTC/USDT:USDT")
|
||||
assert resp1 is None
|
||||
assert delist_fut_mock.call_count == 1
|
||||
|
||||
|
||||
def test__check_delisting_futures_bybit(default_conf_usdt, mocker, markets):
|
||||
markets["BTC/USDT:USDT"] = deepcopy(markets["SOL/BUSD:BUSD"])
|
||||
markets["BTC/USDT:USDT"]["info"]["deliveryTime"] = "0"
|
||||
markets["SOL/BUSD:BUSD"]["info"]["deliveryTime"] = "0"
|
||||
markets["ADA/USDT:USDT"]["info"]["deliveryTime"] = "1760745600000" # 2025-10-18
|
||||
exchange = get_patched_exchange(mocker, default_conf_usdt, exchange="bybit")
|
||||
mocker.patch(f"{EXMS}.markets", PropertyMock(return_value=markets))
|
||||
|
||||
resp_sol = exchange._check_delisting_futures("SOL/BUSD:BUSD")
|
||||
# SOL has no delisting date
|
||||
assert resp_sol is None
|
||||
# Actually has a delisting date
|
||||
resp_ada = exchange._check_delisting_futures("ADA/USDT:USDT")
|
||||
assert resp_ada == dt_utc(2025, 10, 18)
|
||||
|
||||
+318
-28
@@ -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")
|
||||
@@ -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",
|
||||
[
|
||||
@@ -2204,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)
|
||||
|
||||
@@ -2254,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)]
|
||||
@@ -2504,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)
|
||||
@@ -2652,6 +2840,29 @@ def test_refresh_ohlcv_with_cache(mocker, default_conf, time_machine) -> None:
|
||||
assert ohlcv_mock.call_args_list[0][0][0] == pairs
|
||||
|
||||
|
||||
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)
|
||||
async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_name):
|
||||
ohlcv = [
|
||||
@@ -3716,37 +3927,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)
|
||||
|
||||
|
||||
@@ -3931,7 +4134,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,
|
||||
)
|
||||
@@ -5119,6 +5322,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:
|
||||
@@ -5146,6 +5350,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",
|
||||
@@ -5235,8 +5467,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"]))
|
||||
@@ -5280,8 +5517,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")
|
||||
@@ -6368,3 +6610,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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -515,7 +515,8 @@ EXCHANGES = {
|
||||
],
|
||||
},
|
||||
"hyperliquid": {
|
||||
"pair": "UBTC/USDC",
|
||||
# TODO: Should be UBTC/USDC - probably needs a fix in ccxt
|
||||
"pair": "BTC/USDC",
|
||||
"stake_currency": "USDC",
|
||||
"hasQuoteVolume": False,
|
||||
"timeframe": "30m",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -270,11 +270,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 +292,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 +319,8 @@ class TestCCXTExchange:
|
||||
[
|
||||
CandleType.FUTURES,
|
||||
CandleType.FUNDING_RATE,
|
||||
CandleType.INDEX,
|
||||
CandleType.PREMIUMINDEX,
|
||||
CandleType.MARK,
|
||||
],
|
||||
)
|
||||
@@ -322,6 +334,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 +353,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 +367,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 +398,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 +454,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:
|
||||
|
||||
@@ -2548,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,
|
||||
@@ -3092,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),
|
||||
@@ -3751,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,
|
||||
)
|
||||
|
||||
@@ -3805,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,
|
||||
)
|
||||
@@ -3825,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"
|
||||
),
|
||||
),
|
||||
@@ -3836,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
|
||||
@@ -3941,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,
|
||||
@@ -4525,6 +4525,7 @@ def test_check_for_open_trades(mocker, default_conf_usdt, fee, is_short):
|
||||
def test_startup_update_open_orders(mocker, default_conf_usdt, fee, caplog, is_short):
|
||||
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||
create_mock_trades(fee, is_short=is_short)
|
||||
mocker.patch(f"{EXMS}._dry_is_price_crossed", return_value=False)
|
||||
|
||||
freqtrade.startup_update_open_orders()
|
||||
assert not log_has_re(r"Error updating Order .*", caplog)
|
||||
|
||||
@@ -50,16 +50,20 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee,
|
||||
stoploss_order_mock = MagicMock(side_effect=stop_orders)
|
||||
# Sell 3rd trade (not called for the first trade)
|
||||
should_sell_mock = MagicMock(side_effect=[[], [ExitCheckTuple(exit_type=ExitType.EXIT_SIGNAL)]])
|
||||
cancel_order_mock = MagicMock()
|
||||
|
||||
def patch_stoploss(order_id, *args, **kwargs):
|
||||
slo = stoploss_order_open.copy()
|
||||
slo["id"] = order_id
|
||||
slo["status"] = "canceled"
|
||||
return slo
|
||||
|
||||
cancel_order_mock = MagicMock(side_effect=patch_stoploss)
|
||||
mocker.patch.multiple(
|
||||
EXMS,
|
||||
create_stoploss=stoploss,
|
||||
fetch_ticker=ticker,
|
||||
get_fee=fee,
|
||||
amount_to_precision=lambda s, x, y: y,
|
||||
price_to_precision=lambda s, x, y: y,
|
||||
fetch_stoploss_order=stoploss_order_mock,
|
||||
cancel_stoploss_order_with_result=cancel_order_mock,
|
||||
)
|
||||
|
||||
mocker.patch.multiple(
|
||||
@@ -73,6 +77,12 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee,
|
||||
mocker.patch("freqtrade.wallets.Wallets.check_exit_amount", return_value=True)
|
||||
|
||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||
mocker.patch.multiple(
|
||||
freqtrade.exchange,
|
||||
create_stoploss=stoploss,
|
||||
fetch_stoploss_order=stoploss_order_mock,
|
||||
cancel_stoploss_order_with_result=cancel_order_mock,
|
||||
)
|
||||
freqtrade.strategy.order_types["stoploss_on_exchange"] = True
|
||||
# Switch ordertype to market to close trade immediately
|
||||
freqtrade.strategy.order_types["exit"] = "market"
|
||||
@@ -793,9 +803,13 @@ def test_dca_handle_similar_open_order(
|
||||
# Should Create a new exit order
|
||||
freqtrade.exchange.amount_to_contract_precision = MagicMock(return_value=2)
|
||||
freqtrade.strategy.adjust_trade_position = MagicMock(return_value=-2)
|
||||
msg = r"Skipping cancelling stoploss on exchange for.*"
|
||||
|
||||
mocker.patch(f"{EXMS}._dry_is_price_crossed", return_value=False)
|
||||
assert not log_has_re(msg, caplog)
|
||||
freqtrade.process()
|
||||
assert log_has_re(msg, caplog)
|
||||
|
||||
trade = Trade.get_trades().first()
|
||||
|
||||
assert trade.orders[-2].status == "closed"
|
||||
|
||||
@@ -103,7 +103,7 @@ def test_handle_stoploss_on_exchange(
|
||||
trade.is_open = True
|
||||
|
||||
hanging_stoploss_order = MagicMock(return_value={"id": "13434334", "status": "open"})
|
||||
mocker.patch(f"{EXMS}.fetch_stoploss_order", hanging_stoploss_order)
|
||||
mocker.patch.object(freqtrade.exchange, "fetch_stoploss_order", hanging_stoploss_order)
|
||||
|
||||
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
||||
hanging_stoploss_order.assert_called_once_with("13434334", trade.pair)
|
||||
@@ -116,7 +116,7 @@ def test_handle_stoploss_on_exchange(
|
||||
trade.is_open = True
|
||||
|
||||
canceled_stoploss_order = MagicMock(return_value={"id": "13434334", "status": "canceled"})
|
||||
mocker.patch(f"{EXMS}.fetch_stoploss_order", canceled_stoploss_order)
|
||||
mocker.patch.object(freqtrade.exchange, "fetch_stoploss_order", canceled_stoploss_order)
|
||||
stoploss.reset_mock()
|
||||
amount_before = trade.amount
|
||||
|
||||
@@ -149,7 +149,7 @@ def test_handle_stoploss_on_exchange(
|
||||
"amount": enter_order["amount"],
|
||||
}
|
||||
)
|
||||
mocker.patch(f"{EXMS}.fetch_stoploss_order", stoploss_order_hit)
|
||||
mocker.patch.object(freqtrade.exchange, "fetch_stoploss_order", stoploss_order_hit)
|
||||
freqtrade.strategy.order_filled = MagicMock(return_value=None)
|
||||
assert freqtrade.handle_stoploss_on_exchange(trade) is True
|
||||
assert log_has_re(r"STOP_LOSS_LIMIT is hit for Trade\(id=1, .*\)\.", caplog)
|
||||
@@ -158,7 +158,7 @@ def test_handle_stoploss_on_exchange(
|
||||
assert freqtrade.strategy.order_filled.call_count == 1
|
||||
caplog.clear()
|
||||
|
||||
mocker.patch(f"{EXMS}.create_stoploss", side_effect=ExchangeError())
|
||||
mocker.patch.object(freqtrade.exchange, "create_stoploss", side_effect=ExchangeError())
|
||||
trade.is_open = True
|
||||
freqtrade.handle_stoploss_on_exchange(trade)
|
||||
assert log_has("Unable to place a stoploss order on exchange.", caplog)
|
||||
@@ -168,8 +168,13 @@ def test_handle_stoploss_on_exchange(
|
||||
# It should try to add stoploss order
|
||||
stop_order_dict.update({"id": "105"})
|
||||
stoploss.reset_mock()
|
||||
mocker.patch(f"{EXMS}.fetch_stoploss_order", side_effect=InvalidOrderException())
|
||||
mocker.patch(f"{EXMS}.create_stoploss", stoploss)
|
||||
mocker.patch.multiple(
|
||||
freqtrade.exchange,
|
||||
fetch_stoploss_order=MagicMock(
|
||||
side_effect=InvalidOrderException(),
|
||||
),
|
||||
create_stoploss=stoploss,
|
||||
)
|
||||
freqtrade.handle_stoploss_on_exchange(trade)
|
||||
assert len(trade.open_sl_orders) == 1
|
||||
assert stoploss.call_count == 1
|
||||
@@ -179,8 +184,7 @@ def test_handle_stoploss_on_exchange(
|
||||
trade.is_open = False
|
||||
trade.open_sl_orders[-1].ft_is_open = False
|
||||
stoploss.reset_mock()
|
||||
mocker.patch(f"{EXMS}.fetch_order")
|
||||
mocker.patch(f"{EXMS}.create_stoploss", stoploss)
|
||||
mocker.patch.multiple(freqtrade.exchange, fetch_order=MagicMock(), create_stoploss=stoploss)
|
||||
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
||||
assert trade.has_open_sl_orders is False
|
||||
assert stoploss.call_count == 0
|
||||
@@ -252,9 +256,12 @@ def test_handle_stoploss_on_exchange_emergency(
|
||||
stoploss = MagicMock(side_effect=InvalidOrderException())
|
||||
assert trade.has_open_sl_orders is True
|
||||
Trade.commit()
|
||||
mocker.patch(f"{EXMS}.cancel_stoploss_order_with_result", side_effect=InvalidOrderException())
|
||||
mocker.patch(f"{EXMS}.fetch_stoploss_order", stoploss_order_cancelled)
|
||||
mocker.patch(f"{EXMS}.create_stoploss", stoploss)
|
||||
mocker.patch.multiple(
|
||||
freqtrade.exchange,
|
||||
cancel_stoploss_order_with_result=MagicMock(side_effect=InvalidOrderException()),
|
||||
fetch_stoploss_order=stoploss_order_cancelled,
|
||||
create_stoploss=stoploss,
|
||||
)
|
||||
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
||||
assert trade.has_open_sl_orders is False
|
||||
assert trade.is_open is False
|
||||
@@ -311,7 +318,7 @@ def test_handle_stoploss_on_exchange_partial(
|
||||
"amount": enter_order["amount"],
|
||||
}
|
||||
)
|
||||
mocker.patch(f"{EXMS}.fetch_stoploss_order", stoploss_order_hit)
|
||||
mocker.patch.multiple(freqtrade.exchange, fetch_stoploss_order=stoploss_order_hit)
|
||||
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
||||
# Stoploss filled partially ...
|
||||
assert trade.amount == 15
|
||||
@@ -383,8 +390,11 @@ def test_handle_stoploss_on_exchange_partial_cancel_here(
|
||||
"amount": enter_order["amount"],
|
||||
}
|
||||
)
|
||||
mocker.patch(f"{EXMS}.fetch_stoploss_order", stoploss_order_hit)
|
||||
mocker.patch(f"{EXMS}.cancel_stoploss_order_with_result", stoploss_order_cancel)
|
||||
mocker.patch.multiple(
|
||||
freqtrade.exchange,
|
||||
fetch_stoploss_order=stoploss_order_hit,
|
||||
cancel_stoploss_order_with_result=stoploss_order_cancel,
|
||||
)
|
||||
time_machine.shift(timedelta(minutes=15))
|
||||
|
||||
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
||||
@@ -408,20 +418,20 @@ def test_handle_sle_cancel_cant_recreate(
|
||||
mocker.patch.multiple(
|
||||
EXMS,
|
||||
fetch_ticker=MagicMock(return_value={"bid": 1.9, "ask": 2.2, "last": 1.9}),
|
||||
get_fee=fee,
|
||||
)
|
||||
freqtrade = FreqtradeBot(default_conf_usdt)
|
||||
mocker.patch.multiple(
|
||||
freqtrade.exchange,
|
||||
create_order=MagicMock(
|
||||
side_effect=[
|
||||
enter_order,
|
||||
exit_order,
|
||||
]
|
||||
),
|
||||
get_fee=fee,
|
||||
)
|
||||
mocker.patch.multiple(
|
||||
EXMS,
|
||||
fetch_stoploss_order=MagicMock(return_value={"status": "canceled", "id": "100"}),
|
||||
create_stoploss=MagicMock(side_effect=ExchangeError()),
|
||||
)
|
||||
freqtrade = FreqtradeBot(default_conf_usdt)
|
||||
patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
|
||||
|
||||
freqtrade.enter_positions()
|
||||
@@ -644,8 +654,11 @@ def test_handle_stoploss_on_exchange_trailing(
|
||||
stoploss_order_cancel = deepcopy(stoploss_order_hanging)
|
||||
stoploss_order_cancel["status"] = "canceled"
|
||||
|
||||
mocker.patch(f"{EXMS}.fetch_stoploss_order", return_value=stoploss_order_hanging)
|
||||
mocker.patch(f"{EXMS}.cancel_stoploss_order", return_value=stoploss_order_cancel)
|
||||
mocker.patch.multiple(
|
||||
freqtrade.exchange,
|
||||
fetch_stoploss_order=MagicMock(return_value=stoploss_order_hanging),
|
||||
cancel_stoploss_order=MagicMock(return_value=stoploss_order_cancel),
|
||||
)
|
||||
|
||||
# stoploss initially at 5%
|
||||
assert freqtrade.handle_trade(trade) is False
|
||||
@@ -671,9 +684,12 @@ def test_handle_stoploss_on_exchange_trailing(
|
||||
return_value={"id": "13434334", "status": "canceled", "fee": {}, "amount": trade.amount}
|
||||
)
|
||||
stoploss_order_mock = MagicMock(return_value={"id": "so1", "status": "open"})
|
||||
mocker.patch(f"{EXMS}.fetch_stoploss_order")
|
||||
mocker.patch(f"{EXMS}.cancel_stoploss_order", cancel_order_mock)
|
||||
mocker.patch(f"{EXMS}.create_stoploss", stoploss_order_mock)
|
||||
mocker.patch.multiple(
|
||||
freqtrade.exchange,
|
||||
fetch_stoploss_order=MagicMock(),
|
||||
cancel_stoploss_order=cancel_order_mock,
|
||||
create_stoploss=stoploss_order_mock,
|
||||
)
|
||||
|
||||
# stoploss should not be updated as the interval is 60 seconds
|
||||
assert freqtrade.handle_trade(trade) is False
|
||||
@@ -711,8 +727,9 @@ def test_handle_stoploss_on_exchange_trailing(
|
||||
}
|
||||
),
|
||||
)
|
||||
mocker.patch(
|
||||
f"{EXMS}.cancel_stoploss_order_with_result",
|
||||
mocker.patch.object(
|
||||
freqtrade.exchange,
|
||||
"cancel_stoploss_order_with_result",
|
||||
return_value={"id": "so1", "status": "canceled"},
|
||||
)
|
||||
assert len(trade.open_sl_orders) == 1
|
||||
@@ -786,8 +803,12 @@ def test_handle_stoploss_on_exchange_trailing_error(
|
||||
order_date=dt_now(),
|
||||
)
|
||||
)
|
||||
mocker.patch(f"{EXMS}.cancel_stoploss_order", side_effect=InvalidOrderException())
|
||||
mocker.patch(f"{EXMS}.fetch_stoploss_order", return_value=stoploss_order_hanging)
|
||||
mocker.patch.object(
|
||||
freqtrade.exchange, "cancel_stoploss_order", side_effect=InvalidOrderException()
|
||||
)
|
||||
mocker.patch.object(
|
||||
freqtrade.exchange, "fetch_stoploss_order", return_value=stoploss_order_hanging
|
||||
)
|
||||
time_machine.shift(timedelta(minutes=50))
|
||||
freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging)
|
||||
assert log_has_re(r"Could not cancel stoploss order abcd for pair ETH/USDT.*", caplog)
|
||||
@@ -799,8 +820,8 @@ def test_handle_stoploss_on_exchange_trailing_error(
|
||||
|
||||
# Fail creating stoploss order
|
||||
caplog.clear()
|
||||
cancel_mock = mocker.patch(f"{EXMS}.cancel_stoploss_order")
|
||||
mocker.patch(f"{EXMS}.create_stoploss", side_effect=ExchangeError())
|
||||
cancel_mock = mocker.patch.object(freqtrade.exchange, "cancel_stoploss_order")
|
||||
mocker.patch.object(freqtrade.exchange, "create_stoploss", side_effect=ExchangeError())
|
||||
time_machine.shift(timedelta(minutes=50))
|
||||
freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging)
|
||||
assert cancel_mock.call_count == 2
|
||||
@@ -846,20 +867,9 @@ def test_handle_stoploss_on_exchange_custom_stop(
|
||||
mocker.patch.multiple(
|
||||
EXMS,
|
||||
fetch_ticker=MagicMock(return_value={"bid": 1.9, "ask": 2.2, "last": 1.9}),
|
||||
create_order=MagicMock(
|
||||
side_effect=[
|
||||
enter_order,
|
||||
exit_order,
|
||||
]
|
||||
),
|
||||
get_fee=fee,
|
||||
is_cancel_order_result_suitable=MagicMock(return_value=True),
|
||||
)
|
||||
mocker.patch.multiple(
|
||||
EXMS,
|
||||
create_stoploss=stoploss,
|
||||
stoploss_adjust=MagicMock(return_value=True),
|
||||
)
|
||||
|
||||
# enabling TSL
|
||||
default_conf_usdt["use_custom_stoploss"] = True
|
||||
@@ -868,6 +878,17 @@ def test_handle_stoploss_on_exchange_custom_stop(
|
||||
default_conf_usdt["minimal_roi"]["0"] = 999999999
|
||||
|
||||
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||
mocker.patch.multiple(
|
||||
freqtrade.exchange,
|
||||
create_order=MagicMock(
|
||||
side_effect=[
|
||||
enter_order,
|
||||
exit_order,
|
||||
]
|
||||
),
|
||||
create_stoploss=stoploss,
|
||||
stoploss_adjust=MagicMock(return_value=True),
|
||||
)
|
||||
|
||||
# enabling stoploss on exchange
|
||||
freqtrade.strategy.order_types["stoploss_on_exchange"] = True
|
||||
@@ -912,8 +933,11 @@ def test_handle_stoploss_on_exchange_custom_stop(
|
||||
x["id"] = order_id
|
||||
return x
|
||||
|
||||
mocker.patch(f"{EXMS}.fetch_stoploss_order", MagicMock(fetch_stoploss_order_mock))
|
||||
mocker.patch(f"{EXMS}.cancel_stoploss_order", return_value=slo_canceled)
|
||||
mocker.patch.multiple(
|
||||
freqtrade.exchange,
|
||||
fetch_stoploss_order=MagicMock(fetch_stoploss_order_mock),
|
||||
cancel_stoploss_order=MagicMock(return_value=slo_canceled),
|
||||
)
|
||||
|
||||
assert freqtrade.handle_trade(trade) is False
|
||||
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
||||
@@ -932,8 +956,11 @@ def test_handle_stoploss_on_exchange_custom_stop(
|
||||
|
||||
cancel_order_mock = MagicMock()
|
||||
stoploss_order_mock = MagicMock(return_value={"id": "so1", "status": "open"})
|
||||
mocker.patch(f"{EXMS}.cancel_stoploss_order", cancel_order_mock)
|
||||
mocker.patch(f"{EXMS}.create_stoploss", stoploss_order_mock)
|
||||
mocker.patch.multiple(
|
||||
freqtrade.exchange,
|
||||
cancel_stoploss_order=cancel_order_mock,
|
||||
create_stoploss=stoploss_order_mock,
|
||||
)
|
||||
|
||||
# stoploss should not be updated as the interval is 60 seconds
|
||||
assert freqtrade.handle_trade(trade) is False
|
||||
@@ -1054,7 +1081,9 @@ def test_execute_trade_exit_sloe_cancel_exception(
|
||||
mocker, default_conf_usdt, ticker_usdt, fee, caplog
|
||||
) -> None:
|
||||
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||
mocker.patch(f"{EXMS}.cancel_stoploss_order", side_effect=InvalidOrderException())
|
||||
mocker.patch.object(
|
||||
freqtrade.exchange, "cancel_stoploss_order", side_effect=InvalidOrderException()
|
||||
)
|
||||
mocker.patch("freqtrade.wallets.Wallets.get_free", MagicMock(return_value=300))
|
||||
create_order_mock = MagicMock(
|
||||
side_effect=[
|
||||
@@ -1114,12 +1143,15 @@ def test_execute_trade_exit_with_stoploss_on_exchange(
|
||||
get_fee=fee,
|
||||
amount_to_precision=lambda s, x, y: y,
|
||||
price_to_precision=lambda s, x, y: y,
|
||||
)
|
||||
freqtrade = FreqtradeBot(default_conf_usdt)
|
||||
mocker.patch.multiple(
|
||||
freqtrade.exchange,
|
||||
create_stoploss=stoploss,
|
||||
cancel_stoploss_order=cancel_order,
|
||||
_dry_is_price_crossed=MagicMock(side_effect=[True, False]),
|
||||
)
|
||||
|
||||
freqtrade = FreqtradeBot(default_conf_usdt)
|
||||
freqtrade.strategy.order_types["stoploss_on_exchange"] = True
|
||||
patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
|
||||
|
||||
@@ -1208,7 +1240,7 @@ def test_may_execute_trade_exit_after_stoploss_on_exchange_hit(
|
||||
"trades": None,
|
||||
}
|
||||
)
|
||||
mocker.patch(f"{EXMS}.fetch_stoploss_order", stoploss_executed)
|
||||
mocker.patch.object(freqtrade.exchange, "fetch_stoploss_order", stoploss_executed)
|
||||
|
||||
freqtrade.exit_positions(trades)
|
||||
assert trade.has_open_sl_orders is False
|
||||
|
||||
@@ -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']
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
|
||||
+13
-5
@@ -99,6 +99,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,
|
||||
@@ -309,7 +310,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 +386,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 +429,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
|
||||
|
||||
@@ -1034,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,
|
||||
)
|
||||
@@ -1605,6 +1604,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(
|
||||
@@ -1817,6 +1818,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": [],
|
||||
}
|
||||
|
||||
@@ -1849,9 +1852,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."}
|
||||
@@ -2754,12 +2783,12 @@ def test_list_available_pairs(botclient):
|
||||
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)
|
||||
@@ -3247,6 +3276,7 @@ def test_api_download_data(botclient, mocker, tmp_path):
|
||||
body = {
|
||||
"pairs": ["ETH/BTC", "XRP/BTC"],
|
||||
"timeframes": ["5m"],
|
||||
"candle_types": ["spot"],
|
||||
}
|
||||
|
||||
# Fail, already running
|
||||
|
||||
@@ -421,7 +421,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
|
||||
|
||||
|
||||
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Binary file not shown.
@@ -20,8 +20,8 @@ def test_binance_mig_data_conversion(default_conf_usdt, tmp_path, testdatadir):
|
||||
files = [
|
||||
"-1h-mark.feather",
|
||||
"-1h-futures.feather",
|
||||
"-8h-funding_rate.feather",
|
||||
"-8h-mark.feather",
|
||||
"-1h-funding_rate.feather",
|
||||
"-1h-mark.feather",
|
||||
]
|
||||
|
||||
# Copy files to tmpdir and rename to old naming
|
||||
|
||||
@@ -108,6 +108,7 @@ def test_format_date() -> None:
|
||||
date = datetime(2021, 9, 30, 22, 59, 3, 455555, tzinfo=UTC)
|
||||
assert format_date(date) == "2021-09-30 22:59:03"
|
||||
assert format_date(None) == ""
|
||||
assert format_date(None, "closed") == "closed"
|
||||
|
||||
|
||||
def test_format_ms_time_detailed() -> None:
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
from datetime import timedelta
|
||||
|
||||
from freqtrade.util import decimals_per_coin, fmt_coin, fmt_coin2, format_duration, round_value
|
||||
from freqtrade.util import (
|
||||
decimals_per_coin,
|
||||
fmt_coin,
|
||||
fmt_coin2,
|
||||
format_duration,
|
||||
format_pct,
|
||||
round_value,
|
||||
)
|
||||
|
||||
|
||||
def test_decimals_per_coin():
|
||||
@@ -25,6 +32,7 @@ def test_fmt_coin():
|
||||
assert fmt_coin(0.1274512123, "BTC", False) == "0.12745121"
|
||||
assert fmt_coin(0.1274512123, "ETH", False) == "0.12745"
|
||||
assert fmt_coin(222.2, "USDT", False, True) == "222.200"
|
||||
assert fmt_coin(float("nan"), "USDT", False, True) == "N/A"
|
||||
|
||||
|
||||
def test_fmt_coin2():
|
||||
@@ -35,6 +43,7 @@ def test_fmt_coin2():
|
||||
assert fmt_coin2(0.1274512123, "BTC") == "0.12745121 BTC"
|
||||
assert fmt_coin2(0.1274512123, "ETH") == "0.12745121 ETH"
|
||||
assert fmt_coin2(0.00001245, "PEPE") == "0.00001245 PEPE"
|
||||
assert fmt_coin2(float("nan"), "PEPE") == "N/A PEPE"
|
||||
|
||||
|
||||
def test_round_value():
|
||||
@@ -46,6 +55,10 @@ def test_round_value():
|
||||
assert round_value(0.1274512123, 5) == "0.12745"
|
||||
assert round_value(222.2, 3, True) == "222.200"
|
||||
assert round_value(222.2, 0, True) == "222"
|
||||
assert round_value(float("nan"), 0, True) == "N/A"
|
||||
assert round_value(float("nan"), 10, True) == "N/A"
|
||||
assert round_value(None, 10, True) == "N/A"
|
||||
assert round_value(None, 1, True) == "N/A"
|
||||
|
||||
|
||||
def test_format_duration():
|
||||
@@ -55,3 +68,13 @@ def test_format_duration():
|
||||
assert format_duration(timedelta(minutes=1445)) == "1d 00:05"
|
||||
assert format_duration(timedelta(minutes=11445)) == "7d 22:45"
|
||||
assert format_duration(timedelta(minutes=101445)) == "70d 10:45"
|
||||
|
||||
|
||||
def test_format_pct():
|
||||
assert format_pct(0.1234) == "12.34%"
|
||||
assert format_pct(0.1) == "10.00%"
|
||||
assert format_pct(0.0) == "0.00%"
|
||||
assert format_pct(-0.0567) == "-5.67%"
|
||||
assert format_pct(-1.5567) == "-155.67%"
|
||||
assert format_pct(None) == "N/A"
|
||||
assert format_pct(float("nan")) == "N/A"
|
||||
|
||||
@@ -5,13 +5,13 @@ from freqtrade.util.migrations import migrate_funding_fee_timeframe
|
||||
|
||||
def test_migrate_funding_rate_timeframe(default_conf_usdt, tmp_path, testdatadir):
|
||||
copytree(testdatadir / "futures", tmp_path / "futures")
|
||||
file_4h = tmp_path / "futures" / "XRP_USDT_USDT-4h-funding_rate.feather"
|
||||
file_8h = tmp_path / "futures" / "XRP_USDT_USDT-8h-funding_rate.feather"
|
||||
file_30m = tmp_path / "futures" / "XRP_USDT_USDT-30m-funding_rate.feather"
|
||||
file_1h_fr = tmp_path / "futures" / "XRP_USDT_USDT-1h-funding_rate.feather"
|
||||
file_1h = tmp_path / "futures" / "XRP_USDT_USDT-1h-futures.feather"
|
||||
file_8h.rename(file_4h)
|
||||
file_1h_fr.rename(file_30m)
|
||||
assert file_1h.exists()
|
||||
assert file_4h.exists()
|
||||
assert not file_8h.exists()
|
||||
assert file_30m.exists()
|
||||
assert not file_1h_fr.exists()
|
||||
|
||||
default_conf_usdt["datadir"] = tmp_path
|
||||
|
||||
@@ -22,7 +22,7 @@ def test_migrate_funding_rate_timeframe(default_conf_usdt, tmp_path, testdatadir
|
||||
|
||||
migrate_funding_fee_timeframe(default_conf_usdt, None)
|
||||
|
||||
assert not file_4h.exists()
|
||||
assert file_8h.exists()
|
||||
assert not file_30m.exists()
|
||||
assert file_1h_fr.exists()
|
||||
# futures files is untouched.
|
||||
assert file_1h.exists()
|
||||
|
||||
Reference in New Issue
Block a user