Merge branch 'develop' into fix/orderflow_cache

This commit is contained in:
Matthias
2024-12-11 19:27:39 +01:00
54 changed files with 1404 additions and 401 deletions

View File

@@ -1,6 +1,7 @@
# pragma pylint: disable=missing-docstring
import json
import logging
import platform
import re
from copy import deepcopy
from datetime import datetime, timedelta, timezone
@@ -517,6 +518,30 @@ def patch_gc(mocker) -> None:
mocker.patch("freqtrade.main.gc_set_threshold")
def is_arm() -> bool:
machine = platform.machine()
return "arm" in machine or "aarch64" in machine
def is_mac() -> bool:
machine = platform.system()
return "Darwin" in machine
@pytest.fixture(autouse=True)
def patch_torch_initlogs(mocker) -> None:
if is_mac():
# Mock torch import completely
import sys
import types
module_name = "torch"
mocked_module = types.ModuleType(module_name)
sys.modules[module_name] = mocked_module
else:
mocker.patch("torch._logging._init_logs")
@pytest.fixture(autouse=True)
def user_dir(mocker, tmp_path) -> Path:
user_dir = tmp_path / "user_data"
@@ -2212,7 +2237,7 @@ def tickers():
"first": None,
"last": 8603.67,
"change": -0.879,
"percentage": None,
"percentage": -8.95,
"average": None,
"baseVolume": 30414.604298,
"quoteVolume": 259629896.48584127,
@@ -2256,7 +2281,7 @@ def tickers():
"first": None,
"last": 129.28,
"change": 1.795,
"percentage": None,
"percentage": -2.5,
"average": None,
"baseVolume": 59698.79897,
"quoteVolume": 29132399.743954,

View File

@@ -293,6 +293,7 @@ def test_liquidation_price_binance(
default_conf["trading_mode"] = trading_mode
default_conf["margin_mode"] = margin_mode
default_conf["liquidation_buffer"] = 0.0
mocker.patch(f"{EXMS}.price_to_precision", lambda s, x, y, **kwargs: y)
exchange = get_patched_exchange(mocker, default_conf, exchange="binance")
def get_maint_ratio(pair_, stake_amount):

View File

@@ -2006,6 +2006,46 @@ def test_get_tickers(default_conf, mocker, exchange_name, caplog):
assert exchange.get_tickers() == {}
@pytest.mark.parametrize("exchange_name", EXCHANGES)
def test_get_conversion_rate(default_conf_usdt, mocker, exchange_name):
api_mock = MagicMock()
tick = {
"ETH/USDT": {
"last": 42,
},
"BCH/USDT": {
"last": 41,
},
"ETH/BTC": {
"last": 250,
},
}
tick2 = {
"ADA/USDT:USDT": {
"last": 2.5,
}
}
mocker.patch(f"{EXMS}.exchange_has", return_value=True)
api_mock.fetch_tickers = MagicMock(side_effect=[tick, tick2])
api_mock.fetch_bids_asks = MagicMock(return_value={})
exchange = get_patched_exchange(mocker, default_conf_usdt, api_mock, exchange=exchange_name)
# retrieve original ticker
assert exchange.get_conversion_rate("USDT", "USDT") == 1
assert api_mock.fetch_tickers.call_count == 0
assert exchange.get_conversion_rate("ETH", "USDT") == 42
assert exchange.get_conversion_rate("ETH", "USDC") is None
assert exchange.get_conversion_rate("ETH", "BTC") == 250
assert exchange.get_conversion_rate("BTC", "ETH") == 0.004
assert api_mock.fetch_tickers.call_count == 1
api_mock.fetch_tickers.reset_mock()
assert exchange.get_conversion_rate("ADA", "USDT") == 2.5
# Only the call to the "others" market
assert api_mock.fetch_tickers.call_count == 1
@pytest.mark.parametrize("exchange_name", EXCHANGES)
def test_fetch_ticker(default_conf, mocker, exchange_name):
api_mock = MagicMock()
@@ -4079,10 +4119,16 @@ def test_get_valid_pair_combination(default_conf, mocker, markets):
)
ex = Exchange(default_conf)
assert ex.get_valid_pair_combination("ETH", "BTC") == "ETH/BTC"
assert ex.get_valid_pair_combination("BTC", "ETH") == "ETH/BTC"
assert next(ex.get_valid_pair_combination("ETH", "BTC")) == "ETH/BTC"
assert next(ex.get_valid_pair_combination("BTC", "ETH")) == "ETH/BTC"
multicombs = list(ex.get_valid_pair_combination("ETH", "USDT"))
assert len(multicombs) == 2
assert "ETH/USDT" in multicombs
assert "ETH/USDT:USDT" in multicombs
with pytest.raises(ValueError, match=r"Could not combine.* to get a valid pair."):
ex.get_valid_pair_combination("NOPAIR", "ETH")
for x in ex.get_valid_pair_combination("NOPAIR", "ETH"):
pass
@pytest.mark.parametrize(
@@ -6131,6 +6177,7 @@ def test_get_liquidation_price(
default_conf_usdt["exchange"]["name"] = exchange_name
default_conf_usdt["margin_mode"] = margin_mode
mocker.patch("freqtrade.exchange.gate.Gate.validate_ordertypes")
mocker.patch(f"{EXMS}.price_to_precision", lambda s, x, y, **kwargs: y)
exchange = get_patched_exchange(mocker, default_conf_usdt, exchange=exchange_name)
exchange.get_maintenance_ratio_and_amt = MagicMock(return_value=(0.01, 0.01))

View File

@@ -1,4 +1,3 @@
import platform
import sys
from copy import deepcopy
from pathlib import Path
@@ -20,30 +19,6 @@ def is_py12() -> bool:
return sys.version_info >= (3, 12)
def is_mac() -> bool:
machine = platform.system()
return "Darwin" in machine
def is_arm() -> bool:
machine = platform.machine()
return "arm" in machine or "aarch64" in machine
@pytest.fixture(autouse=True)
def patch_torch_initlogs(mocker) -> None:
if is_mac():
# Mock torch import completely
import sys
import types
module_name = "torch"
mocked_module = types.ModuleType(module_name)
sys.modules[module_name] = mocked_module
else:
mocker.patch("torch._logging._init_logs")
@pytest.fixture(scope="function")
def freqai_conf(default_conf, tmp_path):
freqaiconf = deepcopy(default_conf)

View File

@@ -10,11 +10,10 @@ from freqtrade.configuration import TimeRange
from freqtrade.data.dataprovider import DataProvider
from freqtrade.exceptions import OperationalException
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
from tests.conftest import get_patched_exchange
from tests.conftest import get_patched_exchange, is_mac
from tests.freqai.conftest import (
get_patched_data_kitchen,
get_patched_freqai_strategy,
is_mac,
make_unfiltered_dataframe,
)

View File

@@ -13,11 +13,16 @@ from freqtrade.freqai.utils import download_all_data_for_training, get_required_
from freqtrade.optimize.backtesting import Backtesting
from freqtrade.persistence import Trade
from freqtrade.plugins.pairlistmanager import PairListManager
from tests.conftest import EXMS, create_mock_trades, get_patched_exchange, log_has_re
from tests.freqai.conftest import (
get_patched_freqai_strategy,
from tests.conftest import (
EXMS,
create_mock_trades,
get_patched_exchange,
is_arm,
is_mac,
log_has_re,
)
from tests.freqai.conftest import (
get_patched_freqai_strategy,
make_rl_config,
mock_pytorch_mlp_model_training_parameters,
)

View File

@@ -4021,7 +4021,7 @@ def test_get_real_amount_fees_order(
default_conf_usdt, market_buy_order_usdt_doublefee, fee, mocker
):
tfo_mock = mocker.patch(f"{EXMS}.get_trades_for_order", return_value=[])
mocker.patch(f"{EXMS}.get_valid_pair_combination", return_value="BNB/USDT")
mocker.patch(f"{EXMS}.get_valid_pair_combination", return_value=["BNB/USDT"])
mocker.patch(f"{EXMS}.fetch_ticker", return_value={"last": 200})
trade = Trade(
pair="LTC/USDT",

View File

@@ -29,7 +29,7 @@ def test_update_liquidation_prices(mocker, margin_mode, dry_run):
assert trade_mock.set_liquidation_price.call_count == 1
assert wallets.get_total.call_count == (
assert wallets.get_collateral.call_count == (
0 if margin_mode == MarginMode.ISOLATED or not dry_run else 1
)

View File

@@ -568,6 +568,7 @@ def test_backtest__enter_trade_futures(default_conf_usdt, fee, mocker) -> None:
mocker.patch(f"{EXMS}.get_fee", fee)
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"))
mocker.patch(f"{EXMS}.price_to_precision", lambda s, x, y, **kwargs: y)
mocker.patch(f"{EXMS}.get_max_leverage", return_value=100)
mocker.patch("freqtrade.optimize.backtesting.price_to_precision", lambda p, *args: p)
patch_exchange(mocker)
@@ -1842,6 +1843,7 @@ def test_backtest_multi_pair_long_short_switch(
if use_detail:
default_conf_usdt["timeframe_detail"] = "1m"
mocker.patch(f"{EXMS}.price_to_precision", lambda s, x, y, **kwargs: y)
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"))
mocker.patch(f"{EXMS}.get_fee", fee)

View File

@@ -39,13 +39,34 @@ def test_loss_calculation_prefer_correct_trade_count(hyperopt_conf, hyperopt_res
hyperopt_conf.update({"hyperopt_loss": "ShortTradeDurHyperOptLoss"})
hl = HyperOptLossResolver.load_hyperoptloss(hyperopt_conf)
correct = hl.hyperopt_loss_function(
hyperopt_results, 600, datetime(2019, 1, 1), datetime(2019, 5, 1)
results=hyperopt_results,
trade_count=600,
min_date=datetime(2019, 1, 1),
max_date=datetime(2019, 5, 1),
config=hyperopt_conf,
processed=None,
backtest_stats={"profit_total": hyperopt_results["profit_abs"].sum()},
starting_balance=hyperopt_conf["dry_run_wallet"],
)
over = hl.hyperopt_loss_function(
hyperopt_results, 600 + 100, datetime(2019, 1, 1), datetime(2019, 5, 1)
results=hyperopt_results,
trade_count=600 + 100,
min_date=datetime(2019, 1, 1),
max_date=datetime(2019, 5, 1),
config=hyperopt_conf,
processed=None,
backtest_stats={"profit_total": hyperopt_results["profit_abs"].sum()},
starting_balance=hyperopt_conf["dry_run_wallet"],
)
under = hl.hyperopt_loss_function(
hyperopt_results, 600 - 100, datetime(2019, 1, 1), datetime(2019, 5, 1)
results=hyperopt_results,
trade_count=600 - 100,
min_date=datetime(2019, 1, 1),
max_date=datetime(2019, 5, 1),
config=hyperopt_conf,
processed=None,
backtest_stats={"profit_total": hyperopt_results["profit_abs"].sum()},
starting_balance=hyperopt_conf["dry_run_wallet"],
)
assert over > correct
assert under > correct
@@ -58,9 +79,25 @@ def test_loss_calculation_prefer_shorter_trades(hyperopt_conf, hyperopt_results)
hyperopt_conf.update({"hyperopt_loss": "ShortTradeDurHyperOptLoss"})
hl = HyperOptLossResolver.load_hyperoptloss(hyperopt_conf)
longer = hl.hyperopt_loss_function(
hyperopt_results, 100, datetime(2019, 1, 1), datetime(2019, 5, 1)
results=hyperopt_results,
trade_count=100,
min_date=datetime(2019, 1, 1),
max_date=datetime(2019, 5, 1),
config=hyperopt_conf,
processed=None,
backtest_stats={"profit_total": hyperopt_results["profit_abs"].sum()},
starting_balance=hyperopt_conf["dry_run_wallet"],
)
shorter = hl.hyperopt_loss_function(
results=resultsb,
trade_count=100,
min_date=datetime(2019, 1, 1),
max_date=datetime(2019, 5, 1),
config=hyperopt_conf,
processed=None,
backtest_stats={"profit_total": resultsb["profit_abs"].sum()},
starting_balance=hyperopt_conf["dry_run_wallet"],
)
shorter = hl.hyperopt_loss_function(resultsb, 100, datetime(2019, 1, 1), datetime(2019, 5, 1))
assert shorter < longer
@@ -73,11 +110,34 @@ def test_loss_calculation_has_limited_profit(hyperopt_conf, hyperopt_results) ->
hyperopt_conf.update({"hyperopt_loss": "ShortTradeDurHyperOptLoss"})
hl = HyperOptLossResolver.load_hyperoptloss(hyperopt_conf)
correct = hl.hyperopt_loss_function(
hyperopt_results, 600, datetime(2019, 1, 1), datetime(2019, 5, 1)
results=hyperopt_results,
trade_count=600,
min_date=datetime(2019, 1, 1),
max_date=datetime(2019, 5, 1),
config=hyperopt_conf,
processed=None,
backtest_stats={"profit_total": hyperopt_results["profit_abs"].sum()},
starting_balance=hyperopt_conf["dry_run_wallet"],
)
over = hl.hyperopt_loss_function(
results=results_over,
trade_count=600,
min_date=datetime(2019, 1, 1),
max_date=datetime(2019, 5, 1),
config=hyperopt_conf,
processed=None,
backtest_stats={"profit_total": results_over["profit_abs"].sum()},
starting_balance=hyperopt_conf["dry_run_wallet"],
)
over = hl.hyperopt_loss_function(results_over, 600, datetime(2019, 1, 1), datetime(2019, 5, 1))
under = hl.hyperopt_loss_function(
results_under, 600, datetime(2019, 1, 1), datetime(2019, 5, 1)
results=results_under,
trade_count=600,
min_date=datetime(2019, 1, 1),
max_date=datetime(2019, 5, 1),
config=hyperopt_conf,
processed=None,
backtest_stats={"profit_total": results_under["profit_abs"].sum()},
starting_balance=hyperopt_conf["dry_run_wallet"],
)
assert over < correct
assert under > correct
@@ -109,31 +169,34 @@ def test_loss_functions_better_profits(default_conf, hyperopt_results, lossfunct
default_conf.update({"hyperopt_loss": lossfunction})
hl = HyperOptLossResolver.load_hyperoptloss(default_conf)
correct = hl.hyperopt_loss_function(
hyperopt_results,
results=hyperopt_results,
trade_count=len(hyperopt_results),
min_date=datetime(2019, 1, 1),
max_date=datetime(2019, 5, 1),
config=default_conf,
processed=None,
backtest_stats={"profit_total": hyperopt_results["profit_abs"].sum()},
starting_balance=default_conf["dry_run_wallet"],
)
over = hl.hyperopt_loss_function(
results_over,
results=results_over,
trade_count=len(results_over),
min_date=datetime(2019, 1, 1),
max_date=datetime(2019, 5, 1),
config=default_conf,
processed=None,
backtest_stats={"profit_total": results_over["profit_abs"].sum()},
starting_balance=default_conf["dry_run_wallet"],
)
under = hl.hyperopt_loss_function(
results_under,
results=results_under,
trade_count=len(results_under),
min_date=datetime(2019, 1, 1),
max_date=datetime(2019, 5, 1),
config=default_conf,
processed=None,
backtest_stats={"profit_total": results_under["profit_abs"].sum()},
starting_balance=default_conf["dry_run_wallet"],
)
assert over < correct
assert under > correct

View File

@@ -360,9 +360,16 @@ def test_gen_pairlist_from_tickers(mocker, rpl_config, tickers):
exchange = get_patched_exchange(mocker, rpl_config, exchange="binance")
pairlistmanager = PairListManager(exchange, rpl_config)
remote_pairlist = PercentChangePairList(
exchange, pairlistmanager, rpl_config, rpl_config["pairlists"][0], 0
)
remote_pairlist = pairlistmanager._pairlist_handlers[0]
# The generator returns BTC ETH and TKN - filtering the first ensures removing pairs
# in this step ain't problematic.
def _validate_pair(pair, ticker):
if pair == "BTC/USDT":
return False
return True
remote_pairlist._validate_pair = _validate_pair
result = remote_pairlist.gen_pairlist(tickers.return_value)

View File

@@ -514,8 +514,13 @@ def test_rpc_balance_handle_error(default_conf, mocker):
patch_get_signal(freqtradebot)
rpc = RPC(freqtradebot)
rpc._fiat_converter = CryptoToFiatConverter({})
with pytest.raises(RPCException, match="Error getting current tickers."):
rpc._rpc_balance(default_conf["stake_currency"], default_conf["fiat_display_currency"])
res = rpc._rpc_balance(default_conf["stake_currency"], default_conf["fiat_display_currency"])
assert res["stake"] == "BTC"
assert len(res["currencies"]) == 1
assert res["currencies"][0]["currency"] == "BTC"
# ETH has not been converted.
assert all(currency["currency"] != "ETH" for currency in res["currencies"])
def test_rpc_balance_handle(default_conf_usdt, mocker, tickers):
@@ -530,6 +535,13 @@ def test_rpc_balance_handle(default_conf_usdt, mocker, tickers):
"total": 5.0,
"used": 4.0,
},
# Invalid coin not in tickers list.
# This triggers a 2nd call to get_tickers
"NotACoin": {
"free": 0.0,
"total": 2.0,
"used": 0.0,
},
"USDT": {
"free": 50.0,
"total": 100.0,
@@ -574,7 +586,7 @@ def test_rpc_balance_handle(default_conf_usdt, mocker, tickers):
fetch_positions=MagicMock(return_value=mock_pos),
get_tickers=tickers,
get_valid_pair_combination=MagicMock(
side_effect=lambda a, b: f"{b}/{a}" if a == "USDT" else f"{a}/{b}"
side_effect=lambda a, b: [f"{b}/{a}" if a == "USDT" else f"{a}/{b}"]
),
)
default_conf_usdt["dry_run"] = False
@@ -590,8 +602,10 @@ def test_rpc_balance_handle(default_conf_usdt, mocker, tickers):
assert pytest.approx(result["total"]) == 2824.83464
assert pytest.approx(result["value"]) == 2824.83464 * 1.2
assert tickers.call_count == 1
assert tickers.call_count == 4
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"
assert "USD" == result["symbol"]
assert result["currencies"] == [
{
@@ -622,6 +636,20 @@ def test_rpc_balance_handle(default_conf_usdt, mocker, tickers):
"is_bot_managed": False,
"is_position": False,
},
{
"currency": "NotACoin",
"balance": 2.0,
"bot_owned": 0,
"est_stake": 0,
"est_stake_bot": 0,
"free": 0.0,
"is_bot_managed": False,
"is_position": False,
"position": 0,
"side": "long",
"stake": "USDT",
"used": 0.0,
},
{
"currency": "USDT",
"free": 50.0,

View File

@@ -556,7 +556,7 @@ def test_api_balance(botclient, mocker, rpc_balance, tickers):
ftbot.config["dry_run"] = False
mocker.patch(f"{EXMS}.get_balances", return_value=rpc_balance)
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_valid_pair_combination", side_effect=lambda a, b: [f"{a}/{b}"])
ftbot.wallets.update()
rc = client_get(client, f"{BASE_URI}/balance")

View File

@@ -960,7 +960,7 @@ async def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance
default_conf["dry_run"] = False
mocker.patch(f"{EXMS}.get_balances", return_value=rpc_balance)
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_valid_pair_combination", side_effect=lambda a, b: [f"{a}/{b}"])
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
patch_get_signal(freqtradebot)
@@ -1049,7 +1049,7 @@ async def test_telegram_balance_handle_futures(
mocker.patch(f"{EXMS}.get_balances", return_value=rpc_balance)
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_valid_pair_combination", side_effect=lambda a, b: [f"{a}/{b}"])
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
patch_get_signal(freqtradebot)

View File

@@ -1481,6 +1481,12 @@ def test_flat_vars_to_nested_dict(caplog):
"FREQTRADE__STAKE_AMOUNT": "200.05",
"FREQTRADE__TELEGRAM__CHAT_ID": "2151",
"NOT_RELEVANT": "200.0", # Will be ignored
"FREQTRADE__ARRAY": '[{"name":"default","host":"xxx"}]',
"FREQTRADE__EXCHANGE__PAIR_WHITELIST": '["BTC/USDT", "ETH/USDT"]',
# Fails due to trailing comma
"FREQTRADE__ARRAY_TRAIL_COMMA": '[{"name":"default","host":"xxx",}]',
# Object fails
"FREQTRADE__OBJECT": '{"name":"default","host":"xxx"}',
}
expected = {
"stake_amount": 200.05,
@@ -1494,8 +1500,12 @@ def test_flat_vars_to_nested_dict(caplog):
},
"some_setting": True,
"some_false_setting": False,
"pair_whitelist": ["BTC/USDT", "ETH/USDT"],
},
"telegram": {"chat_id": "2151"},
"array": [{"name": "default", "host": "xxx"}],
"object": '{"name":"default","host":"xxx"}',
"array_trail_comma": '[{"name":"default","host":"xxx",}]',
}
res = _flat_vars_to_nested_dict(test_args, ENV_VAR_PREFIX)
assert res == expected

View File

@@ -168,7 +168,7 @@ def test_get_trade_stake_amount_unlimited_amount(
assert result == 0
freqtrade.config["dry_run_wallet"] = 200
freqtrade.wallets._start_cap = 200
freqtrade.wallets._start_cap["BTC"] = 200
result = freqtrade.wallets.get_trade_stake_amount("XRP/USDT", 3)
assert round(result, 4) == round(result2, 4)
@@ -451,3 +451,151 @@ def test_check_exit_amount_futures(mocker, default_conf, fee):
assert freqtrade.wallets.check_exit_amount(trade) is False
assert total_mock.call_count == 0
assert update_mock.call_count == 1
@pytest.mark.parametrize(
"config,wallets",
[
(
{"stake_currency": "USDT", "dry_run_wallet": 1000.0},
{"USDT": {"currency": "USDT", "free": 1000.0, "used": 0.0, "total": 1000.0}},
),
(
{"stake_currency": "USDT", "dry_run_wallet": {"USDT": 1000.0, "BTC": 0.1, "ETH": 2.0}},
{
"USDT": {"currency": "USDT", "free": 1000.0, "used": 0.0, "total": 1000.0},
"BTC": {"currency": "BTC", "free": 0.1, "used": 0.0, "total": 0.1},
"ETH": {"currency": "ETH", "free": 2.0, "used": 0.0, "total": 2.0},
},
),
(
{
"stake_currency": "USDT",
"margin_mode": "cross",
"dry_run_wallet": {"USDC": 1000.0, "BTC": 0.1, "ETH": 2.0},
},
{
# USDT wallet should be created with 0 balance, but Free balance, since
# it's converted from the other currencies
"USDT": {"currency": "USDT", "free": 4200.0, "used": 0.0, "total": 0.0},
"USDC": {"currency": "USDC", "free": 1000.0, "used": 0.0, "total": 1000.0},
"BTC": {"currency": "BTC", "free": 0.1, "used": 0.0, "total": 0.1},
"ETH": {"currency": "ETH", "free": 2.0, "used": 0.0, "total": 2.0},
},
),
(
{
"stake_currency": "USDT",
"margin_mode": "cross",
"dry_run_wallet": {"USDT": 500, "USDC": 1000.0, "BTC": 0.1, "ETH": 2.0},
},
{
# USDT wallet should be created with 500 balance, but Free balance, since
# it's converted from the other currencies
"USDT": {"currency": "USDT", "free": 4700.0, "used": 0.0, "total": 500.0},
"USDC": {"currency": "USDC", "free": 1000.0, "used": 0.0, "total": 1000.0},
"BTC": {"currency": "BTC", "free": 0.1, "used": 0.0, "total": 0.1},
"ETH": {"currency": "ETH", "free": 2.0, "used": 0.0, "total": 2.0},
},
),
(
# Same as above, but without cross
{
"stake_currency": "USDT",
"dry_run_wallet": {"USDT": 500, "USDC": 1000.0, "BTC": 0.1, "ETH": 2.0},
},
{
# No "free" transfer for USDT wallet
"USDT": {"currency": "USDT", "free": 500.0, "used": 0.0, "total": 500.0},
"USDC": {"currency": "USDC", "free": 1000.0, "used": 0.0, "total": 1000.0},
"BTC": {"currency": "BTC", "free": 0.1, "used": 0.0, "total": 0.1},
"ETH": {"currency": "ETH", "free": 2.0, "used": 0.0, "total": 2.0},
},
),
(
# Same as above, but with futures and cross
{
"stake_currency": "USDT",
"margin_mode": "cross",
"trading_mode": "futures",
"dry_run_wallet": {"USDT": 500, "USDC": 1000.0, "BTC": 0.1, "ETH": 2.0},
},
{
# USDT wallet should be created with 500 balance, but Free balance, since
# it's converted from the other currencies
"USDT": {"currency": "USDT", "free": 4700.0, "used": 0.0, "total": 500.0},
"USDC": {"currency": "USDC", "free": 1000.0, "used": 0.0, "total": 1000.0},
"BTC": {"currency": "BTC", "free": 0.1, "used": 0.0, "total": 0.1},
"ETH": {"currency": "ETH", "free": 2.0, "used": 0.0, "total": 2.0},
},
),
],
)
def test_dry_run_wallet_initialization(mocker, default_conf_usdt, config, wallets):
default_conf_usdt.update(config)
mocker.patch(
f"{EXMS}.get_tickers",
return_value={
"USDC/USDT": {"last": 1.0},
"BTC/USDT": {"last": 20_000.0},
"ETH/USDT": {"last": 1100.0},
},
)
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
stake_currency = config["stake_currency"]
# Verify each wallet matches the expected values
for currency, expected_wallet in wallets.items():
wallet = freqtrade.wallets._wallets[currency]
assert wallet.currency == expected_wallet["currency"]
assert wallet.free == expected_wallet["free"]
assert wallet.used == expected_wallet["used"]
assert wallet.total == expected_wallet["total"]
# Verify no extra wallets were created
assert len(freqtrade.wallets._wallets) == len(wallets)
# Create a trade and verify the new currency is added to the wallets
mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.0)
mocker.patch(f"{EXMS}.get_rate", return_value=2.22)
mocker.patch(
f"{EXMS}.fetch_ticker",
return_value={
"bid": 0.20,
"ask": 0.22,
"last": 0.22,
},
)
# Without position, collateral will be the same as free
assert freqtrade.wallets.get_collateral() == freqtrade.wallets.get_free(stake_currency)
freqtrade.execute_entry("NEO/USDT", 100.0)
# Update wallets and verify NEO is now included
freqtrade.wallets.update()
if default_conf_usdt["trading_mode"] != "futures":
assert "NEO" in freqtrade.wallets._wallets
assert freqtrade.wallets._wallets["NEO"].total == 45.04504504 # 100 USDT / 0.22
assert freqtrade.wallets._wallets["NEO"].used == 0.0
assert freqtrade.wallets._wallets["NEO"].free == 45.04504504
assert freqtrade.wallets.get_collateral() == freqtrade.wallets.get_free(stake_currency)
# Verify USDT wallet was reduced by trade amount
assert (
pytest.approx(freqtrade.wallets._wallets[stake_currency].total)
== wallets[stake_currency]["total"] - 100.0
)
assert len(freqtrade.wallets._wallets) == len(wallets) + 1 # Original wallets + NEO
else:
# Futures mode
assert "NEO" not in freqtrade.wallets._wallets
assert freqtrade.wallets._positions["NEO/USDT"].position == 45.04504504
assert pytest.approx(freqtrade.wallets._positions["NEO/USDT"].collateral) == 100
# Verify USDT wallet's free was reduced by trade amount
assert (
pytest.approx(freqtrade.wallets.get_collateral())
== freqtrade.wallets.get_free(stake_currency) + 100
)
assert (
pytest.approx(freqtrade.wallets._wallets[stake_currency].free)
== wallets[stake_currency]["free"] - 100.0
)