mirror of
https://github.com/freqtrade/freqtrade.git
synced 2026-01-20 14:00:38 +00:00
Merge pull request #12701 from freqtrade/fix/better_futures_wallets
Enhance futures wallet calculations
This commit is contained in:
@@ -873,8 +873,27 @@ class RPC:
|
|||||||
symbol: str
|
symbol: str
|
||||||
pos: PositionWallet
|
pos: PositionWallet
|
||||||
for symbol, pos in self._freqtrade.wallets.get_all_positions().items():
|
for symbol, pos in self._freqtrade.wallets.get_all_positions().items():
|
||||||
total += pos.collateral
|
est_stake = pos.collateral
|
||||||
total_bot += pos.collateral
|
pos_base = self._freqtrade.exchange.get_pair_base_currency(symbol)
|
||||||
|
if pos.leverage:
|
||||||
|
try:
|
||||||
|
rate = self._freqtrade.exchange.get_conversion_rate(pos_base, stake_currency)
|
||||||
|
if rate:
|
||||||
|
# For a leveraged position, equity (what we want as est_stake) is:
|
||||||
|
# equity = collateral + PnL
|
||||||
|
# notional = rate * pos.position
|
||||||
|
# borrowed = pos.collateral * (pos.leverage - 1)
|
||||||
|
# Equity is notional minus borrowed:
|
||||||
|
# equity = notional - borrowed
|
||||||
|
# = rate * pos.position - pos.collateral * (pos.leverage - 1)
|
||||||
|
est_stake = rate * pos.position - pos.collateral * (pos.leverage - 1)
|
||||||
|
except (ExchangeError, PricingError) as e:
|
||||||
|
logger.warning(f"Error {e} getting rate for futures {symbol} / {pos_base}")
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Add the estimated stake (collateral + unlevered PnL) to totals
|
||||||
|
total += est_stake
|
||||||
|
total_bot += est_stake
|
||||||
|
|
||||||
currencies.append(
|
currencies.append(
|
||||||
{
|
{
|
||||||
@@ -883,11 +902,11 @@ class RPC:
|
|||||||
"balance": 0,
|
"balance": 0,
|
||||||
"used": 0,
|
"used": 0,
|
||||||
"position": pos.position,
|
"position": pos.position,
|
||||||
"est_stake": pos.collateral,
|
"est_stake": est_stake,
|
||||||
"est_stake_bot": pos.collateral,
|
"est_stake_bot": est_stake,
|
||||||
"stake": stake_currency,
|
"stake": stake_currency,
|
||||||
"side": pos.side,
|
"side": pos.side,
|
||||||
"is_bot_managed": True,
|
"is_bot_managed": pos_base in open_assets,
|
||||||
"is_position": True,
|
"is_position": True,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ from tests.conftest import (
|
|||||||
create_mock_trades,
|
create_mock_trades,
|
||||||
create_mock_trades_usdt,
|
create_mock_trades_usdt,
|
||||||
get_patched_freqtradebot,
|
get_patched_freqtradebot,
|
||||||
|
log_has_re,
|
||||||
patch_get_signal,
|
patch_get_signal,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -503,7 +504,7 @@ def test_rpc_trade_statistics(default_conf_usdt, ticker, fee, mocker) -> None:
|
|||||||
assert isnan(stats["profit_all_coin"])
|
assert isnan(stats["profit_all_coin"])
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_balance_handle_error(default_conf, mocker):
|
def test_rpc_balance_handle_error(default_conf, mocker, caplog):
|
||||||
mock_balance = {
|
mock_balance = {
|
||||||
"BTC": {
|
"BTC": {
|
||||||
"free": 10.0,
|
"free": 10.0,
|
||||||
@@ -517,14 +518,42 @@ def test_rpc_balance_handle_error(default_conf, mocker):
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
# ETH will be skipped due to mocked Error below
|
# ETH will be skipped due to mocked Error below
|
||||||
|
mock_pos = [
|
||||||
|
{
|
||||||
|
"symbol": "ADA/USDT:USDT",
|
||||||
|
"timestamp": None,
|
||||||
|
"datetime": None,
|
||||||
|
"initialMargin": 20,
|
||||||
|
"initialMarginPercentage": None,
|
||||||
|
"maintenanceMargin": 0.0,
|
||||||
|
"maintenanceMarginPercentage": 0.005,
|
||||||
|
"entryPrice": 0.0,
|
||||||
|
"notional": 10.0,
|
||||||
|
"leverage": 5.0,
|
||||||
|
"unrealizedPnl": 0.0,
|
||||||
|
"contracts": 1.0,
|
||||||
|
"contractSize": 1,
|
||||||
|
"marginRatio": None,
|
||||||
|
"liquidationPrice": 0.0,
|
||||||
|
"markPrice": 2896.41,
|
||||||
|
# Collateral is in USDT - and can be higher than position size in cross mode
|
||||||
|
"collateral": 50,
|
||||||
|
"marginType": "cross",
|
||||||
|
"side": "short",
|
||||||
|
"percentage": None,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
mocker.patch("freqtrade.rpc.telegram.Telegram", MagicMock())
|
mocker.patch("freqtrade.rpc.telegram.Telegram", MagicMock())
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
EXMS,
|
EXMS,
|
||||||
get_balances=MagicMock(return_value=mock_balance),
|
get_balances=MagicMock(return_value=mock_balance),
|
||||||
|
fetch_positions=MagicMock(return_value=mock_pos),
|
||||||
get_tickers=MagicMock(side_effect=TemporaryError("Could not load ticker due to xxx")),
|
get_tickers=MagicMock(side_effect=TemporaryError("Could not load ticker due to xxx")),
|
||||||
)
|
)
|
||||||
|
default_conf["trading_mode"] = "futures"
|
||||||
|
default_conf["margin_mode"] = "isolated"
|
||||||
|
default_conf["dry_run"] = False
|
||||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||||
patch_get_signal(freqtradebot)
|
patch_get_signal(freqtradebot)
|
||||||
rpc = RPC(freqtradebot)
|
rpc = RPC(freqtradebot)
|
||||||
@@ -533,10 +562,23 @@ def test_rpc_balance_handle_error(default_conf, mocker):
|
|||||||
res = 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 res["stake"] == "BTC"
|
||||||
|
|
||||||
assert len(res["currencies"]) == 1
|
assert len(res["currencies"]) == 3
|
||||||
assert res["currencies"][0]["currency"] == "BTC"
|
assert res["currencies"][0]["currency"] == "BTC"
|
||||||
# ETH has not been converted.
|
curr_ETH = next(currency for currency in res["currencies"] if currency["currency"] == "ETH")
|
||||||
assert all(currency["currency"] != "ETH" for currency in res["currencies"])
|
# coins are part of the result, but were not converted
|
||||||
|
assert curr_ETH is not None
|
||||||
|
assert curr_ETH["currency"] == "ETH"
|
||||||
|
assert curr_ETH["est_stake"] == 0
|
||||||
|
curr_ADA = next(
|
||||||
|
currency for currency in res["currencies"] if currency["currency"] == "ADA/USDT:USDT"
|
||||||
|
)
|
||||||
|
assert curr_ADA is not None
|
||||||
|
assert curr_ADA["currency"] == "ADA/USDT:USDT"
|
||||||
|
# Fall back to collateral value when rate not available
|
||||||
|
assert curr_ADA["est_stake"] == 20
|
||||||
|
|
||||||
|
assert log_has_re(r"Error .* getting rate for futures ADA.*", caplog)
|
||||||
|
assert log_has_re(r"Error .* getting rate for ETH.*", caplog)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("proxy_coin", [None, "BNFCR"])
|
@pytest.mark.parametrize("proxy_coin", [None, "BNFCR"])
|
||||||
@@ -619,15 +661,20 @@ def test_rpc_balance_handle(default_conf_usdt, mocker, tickers, proxy_coin, marg
|
|||||||
rpc = RPC(freqtradebot)
|
rpc = RPC(freqtradebot)
|
||||||
rpc._fiat_converter = CryptoToFiatConverter({})
|
rpc._fiat_converter = CryptoToFiatConverter({})
|
||||||
mocker.patch.object(rpc._fiat_converter, "get_price", return_value=1.2)
|
mocker.patch.object(rpc._fiat_converter, "get_price", return_value=1.2)
|
||||||
|
mocker.patch(
|
||||||
|
"freqtrade.persistence.trade_model.Trade.get_open_trades",
|
||||||
|
return_value=[
|
||||||
|
MagicMock(pair="ETH/USDT:USDT", safe_base_currency="ETH"),
|
||||||
|
],
|
||||||
|
)
|
||||||
result = rpc._rpc_balance(
|
result = rpc._rpc_balance(
|
||||||
default_conf_usdt["stake_currency"], default_conf_usdt["fiat_display_currency"]
|
default_conf_usdt["stake_currency"], default_conf_usdt["fiat_display_currency"]
|
||||||
)
|
)
|
||||||
|
|
||||||
assert tickers.call_count == 4 if not proxy_coin else 6
|
assert tickers.call_count == (7 if proxy_coin and margin_mode != "cross" else 5)
|
||||||
assert tickers.call_args_list[0][1]["cached"] is True
|
assert tickers.call_args_list[0][1]["cached"] is True
|
||||||
# Testing futures - so we should get spot tickers
|
# Testing futures - so we should get spot tickers
|
||||||
assert tickers.call_args_list[-1][1]["market_type"] == "spot"
|
tickers.assert_any_call(symbols=None, cached=True, market_type=TradingMode.SPOT)
|
||||||
assert "USD" == result["symbol"]
|
assert "USD" == result["symbol"]
|
||||||
expected_curr = [
|
expected_curr = [
|
||||||
{
|
{
|
||||||
@@ -692,8 +739,8 @@ def test_rpc_balance_handle(default_conf_usdt, mocker, tickers, proxy_coin, marg
|
|||||||
"balance": 0,
|
"balance": 0,
|
||||||
"used": 0,
|
"used": 0,
|
||||||
"position": 10.0,
|
"position": 10.0,
|
||||||
"est_stake": 20,
|
"est_stake": 5222.1,
|
||||||
"est_stake_bot": 20,
|
"est_stake_bot": 5222.1,
|
||||||
"stake": "USDT",
|
"stake": "USDT",
|
||||||
"side": "short",
|
"side": "short",
|
||||||
"is_bot_managed": True,
|
"is_bot_managed": True,
|
||||||
@@ -755,15 +802,15 @@ def test_rpc_balance_handle(default_conf_usdt, mocker, tickers, proxy_coin, marg
|
|||||||
|
|
||||||
assert result["currencies"] == expected_curr
|
assert result["currencies"] == expected_curr
|
||||||
if proxy_coin and margin_mode == "cross":
|
if proxy_coin and margin_mode == "cross":
|
||||||
assert pytest.approx(result["total_bot"]) == 1505.0
|
assert pytest.approx(result["total_bot"]) == 6707.1
|
||||||
assert pytest.approx(result["total"]) == 2186.6972 # ETH stake is missing.
|
assert pytest.approx(result["total"]) == 7388.7972 # ETH stake is missing.
|
||||||
assert result["starting_capital"] == 1500 * default_conf_usdt["tradable_balance_ratio"]
|
assert result["starting_capital"] == 1500 * default_conf_usdt["tradable_balance_ratio"]
|
||||||
assert result["starting_capital_ratio"] == pytest.approx(0.013468013468013407)
|
assert result["starting_capital_ratio"] == pytest.approx(3.5165656)
|
||||||
else:
|
else:
|
||||||
assert pytest.approx(result["total_bot"]) == 69.5
|
assert pytest.approx(result["total_bot"]) == 5271.6
|
||||||
assert pytest.approx(result["total"]) == 686.6972 # ETH stake is missing.
|
assert pytest.approx(result["total"]) == 5888.7972 # ETH stake is missing.
|
||||||
assert result["starting_capital"] == 50 * default_conf_usdt["tradable_balance_ratio"]
|
assert result["starting_capital"] == 50 * default_conf_usdt["tradable_balance_ratio"]
|
||||||
assert result["starting_capital_ratio"] == pytest.approx(0.4040404)
|
assert result["starting_capital_ratio"] == pytest.approx(105.496969)
|
||||||
assert pytest.approx(result["value"]) == result["total"] * 1.2
|
assert pytest.approx(result["value"]) == result["total"] * 1.2
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1155,7 +1155,7 @@ async def test_telegram_balance_handle_futures(
|
|||||||
"percentage": None,
|
"percentage": None,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"symbol": "XRP/USDT:USDT",
|
"symbol": "ADA/USDT:USDT",
|
||||||
"timestamp": None,
|
"timestamp": None,
|
||||||
"datetime": None,
|
"datetime": None,
|
||||||
"initialMargin": 0.0,
|
"initialMargin": 0.0,
|
||||||
@@ -1181,9 +1181,17 @@ async def test_telegram_balance_handle_futures(
|
|||||||
mocker.patch(f"{EXMS}.fetch_positions", return_value=mock_pos)
|
mocker.patch(f"{EXMS}.fetch_positions", return_value=mock_pos)
|
||||||
mocker.patch(f"{EXMS}.get_tickers", tickers)
|
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}"])
|
||||||
|
mocker.patch(f"{EXMS}.get_conversion_rate", return_value=3200)
|
||||||
|
|
||||||
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
|
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
|
||||||
patch_get_signal(freqtradebot)
|
patch_get_signal(freqtradebot)
|
||||||
|
mocker.patch(
|
||||||
|
"freqtrade.persistence.trade_model.Trade.get_open_trades",
|
||||||
|
return_value=[
|
||||||
|
MagicMock(pair="ETH/USDT:USDT", safe_base_currency="ETH"),
|
||||||
|
MagicMock(pair="ADA/USDT:USDT", safe_base_currency="ADA"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
await telegram._balance(update=update, context=MagicMock())
|
await telegram._balance(update=update, context=MagicMock())
|
||||||
result = msg_mock.call_args_list[0][0][0]
|
result = msg_mock.call_args_list[0][0][0]
|
||||||
@@ -1191,7 +1199,7 @@ async def test_telegram_balance_handle_futures(
|
|||||||
|
|
||||||
assert "ETH/USDT:USDT" in result
|
assert "ETH/USDT:USDT" in result
|
||||||
assert "`short: 10" in result
|
assert "`short: 10" in result
|
||||||
assert "XRP/USDT:USDT" in result
|
assert "ADA/USDT:USDT" in result
|
||||||
|
|
||||||
|
|
||||||
async def test_balance_handle_empty_response(default_conf, update, mocker) -> None:
|
async def test_balance_handle_empty_response(default_conf, update, mocker) -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user