From 3b5f1d52cdb44520b1ea396423fae31e33ee3e9a Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 6 Jan 2026 20:04:11 +0100 Subject: [PATCH 1/6] feat: Futures wallets should consider open PnL --- freqtrade/rpc/rpc.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index c820a5c55..0c6ee4789 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -873,8 +873,21 @@ class RPC: symbol: str pos: PositionWallet for symbol, pos in self._freqtrade.wallets.get_all_positions().items(): - total += pos.collateral - total_bot += pos.collateral + est_stake = 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: + # est_stake = collateral + PnL + 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( { @@ -883,11 +896,11 @@ class RPC: "balance": 0, "used": 0, "position": pos.position, - "est_stake": pos.collateral, - "est_stake_bot": pos.collateral, + "est_stake": est_stake, + "est_stake_bot": est_stake, "stake": stake_currency, "side": pos.side, - "is_bot_managed": True, + "is_bot_managed": pos_base in open_assets, "is_position": True, } ) From 60425e6237d2d38c2ad1d81be38feeb0791c6a5c Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 6 Jan 2026 20:19:18 +0100 Subject: [PATCH 2/6] test: update rpc tests to new calculation mode --- tests/rpc/test_rpc.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index cdcbd331a..cc598e25f 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -624,10 +624,10 @@ def test_rpc_balance_handle(default_conf_usdt, mocker, tickers, proxy_coin, marg default_conf_usdt["stake_currency"], default_conf_usdt["fiat_display_currency"] ) - assert tickers.call_count == 4 if not proxy_coin else 6 + assert tickers.call_count == (7 if proxy_coin and margin_mode != "cross" else 5) assert tickers.call_args_list[0][1]["cached"] is True # Testing futures - so we should get spot tickers - assert tickers.call_args_list[-1][1]["market_type"] == "spot" + tickers.assert_any_call(symbols=None, cached=True, market_type=TradingMode.SPOT) assert "USD" == result["symbol"] expected_curr = [ { @@ -692,11 +692,11 @@ def test_rpc_balance_handle(default_conf_usdt, mocker, tickers, proxy_coin, marg "balance": 0, "used": 0, "position": 10.0, - "est_stake": 20, - "est_stake_bot": 20, + "est_stake": 5222.1, + "est_stake_bot": 5222.1, "stake": "USDT", "side": "short", - "is_bot_managed": True, + "is_bot_managed": False, "is_position": True, }, ] @@ -755,15 +755,15 @@ def test_rpc_balance_handle(default_conf_usdt, mocker, tickers, proxy_coin, marg assert result["currencies"] == expected_curr if proxy_coin and margin_mode == "cross": - assert pytest.approx(result["total_bot"]) == 1505.0 - assert pytest.approx(result["total"]) == 2186.6972 # ETH stake is missing. + assert pytest.approx(result["total_bot"]) == 6707.1 + 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_ratio"] == pytest.approx(0.013468013468013407) + assert result["starting_capital_ratio"] == pytest.approx(3.5165656) else: - assert pytest.approx(result["total_bot"]) == 69.5 - assert pytest.approx(result["total"]) == 686.6972 # ETH stake is missing. + assert pytest.approx(result["total_bot"]) == 5271.6 + 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_ratio"] == pytest.approx(0.4040404) + assert result["starting_capital_ratio"] == pytest.approx(105.496969) assert pytest.approx(result["value"]) == result["total"] * 1.2 From 515912f14d3b64f6313d893f9d352aa1437253a5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 8 Jan 2026 07:24:20 +0100 Subject: [PATCH 3/6] chore: add explaining comment --- freqtrade/rpc/rpc.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 0c6ee4789..cea2e049d 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -879,7 +879,13 @@ class RPC: try: rate = self._freqtrade.exchange.get_conversion_rate(pos_base, stake_currency) if rate: - # est_stake = collateral + PnL + # 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}") From f08f19d1cb3b2dc5731d2131a6d84a2ab6419f9a Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 8 Jan 2026 20:37:12 +0100 Subject: [PATCH 4/6] test: Change test to ADA (it's in markets) Mock trades for balance test --- tests/rpc/test_rpc_telegram.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index fdcac6504..b84497593 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1155,7 +1155,7 @@ async def test_telegram_balance_handle_futures( "percentage": None, }, { - "symbol": "XRP/USDT:USDT", + "symbol": "ADA/USDT:USDT", "timestamp": None, "datetime": None, "initialMargin": 0.0, @@ -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}.get_tickers", tickers) mocker.patch(f"{EXMS}.get_valid_pair_combination", side_effect=lambda a, b: [f"{a}/{b}"]) + mocker.patch(f"{EXMS}.get_conversion_rate", return_value=3200) telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot) + mocker.patch( + "freqtrade.persistence.trade_model.Trade.get_open_trades", + return_value=[ + MagicMock(pair="ETH/USDT:USDT", safe_base_currency="ETH"), + MagicMock(pair="ADA/USDT:USDT", safe_base_currency="ADA"), + ], + ) await telegram._balance(update=update, context=MagicMock()) result = msg_mock.call_args_list[0][0][0] @@ -1191,7 +1199,7 @@ async def test_telegram_balance_handle_futures( assert "ETH/USDT:USDT" in result assert "`short: 10" in result - assert "XRP/USDT:USDT" in result + assert "ADA/USDT:USDT" in result async def test_balance_handle_empty_response(default_conf, update, mocker) -> None: From 06a53924d77ac71d3a7df7ca0b50c24f941677f8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 8 Jan 2026 20:53:29 +0100 Subject: [PATCH 5/6] test: improve balance rpc test --- tests/rpc/test_rpc.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index cc598e25f..4b7f57179 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -619,7 +619,12 @@ def test_rpc_balance_handle(default_conf_usdt, mocker, tickers, proxy_coin, marg rpc = RPC(freqtradebot) rpc._fiat_converter = CryptoToFiatConverter({}) mocker.patch.object(rpc._fiat_converter, "get_price", return_value=1.2) - + mocker.patch( + "freqtrade.persistence.trade_model.Trade.get_open_trades", + return_value=[ + MagicMock(pair="ETH/USDT:USDT", safe_base_currency="ETH"), + ], + ) result = rpc._rpc_balance( default_conf_usdt["stake_currency"], default_conf_usdt["fiat_display_currency"] ) @@ -696,7 +701,7 @@ def test_rpc_balance_handle(default_conf_usdt, mocker, tickers, proxy_coin, marg "est_stake_bot": 5222.1, "stake": "USDT", "side": "short", - "is_bot_managed": False, + "is_bot_managed": True, "is_position": True, }, ] From 31cf2dd46d818f0cdcdad48fd39bd7edb5cc09e1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 9 Jan 2026 06:49:17 +0100 Subject: [PATCH 6/6] test: improve rpc_balance handle error test --- tests/rpc/test_rpc.py | 52 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 4b7f57179..b1d06888c 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -17,6 +17,7 @@ from tests.conftest import ( create_mock_trades, create_mock_trades_usdt, get_patched_freqtradebot, + log_has_re, 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"]) -def test_rpc_balance_handle_error(default_conf, mocker): +def test_rpc_balance_handle_error(default_conf, mocker, caplog): mock_balance = { "BTC": { "free": 10.0, @@ -517,14 +518,42 @@ def test_rpc_balance_handle_error(default_conf, mocker): }, } # ETH will be skipped due to mocked Error below + mock_pos = [ + { + "symbol": "ADA/USDT:USDT", + "timestamp": None, + "datetime": None, + "initialMargin": 20, + "initialMarginPercentage": None, + "maintenanceMargin": 0.0, + "maintenanceMarginPercentage": 0.005, + "entryPrice": 0.0, + "notional": 10.0, + "leverage": 5.0, + "unrealizedPnl": 0.0, + "contracts": 1.0, + "contractSize": 1, + "marginRatio": None, + "liquidationPrice": 0.0, + "markPrice": 2896.41, + # Collateral is in USDT - and can be higher than position size in cross mode + "collateral": 50, + "marginType": "cross", + "side": "short", + "percentage": None, + } + ] mocker.patch("freqtrade.rpc.telegram.Telegram", MagicMock()) mocker.patch.multiple( EXMS, get_balances=MagicMock(return_value=mock_balance), + fetch_positions=MagicMock(return_value=mock_pos), get_tickers=MagicMock(side_effect=TemporaryError("Could not load ticker due to xxx")), ) - + default_conf["trading_mode"] = "futures" + default_conf["margin_mode"] = "isolated" + default_conf["dry_run"] = False freqtradebot = get_patched_freqtradebot(mocker, default_conf) patch_get_signal(freqtradebot) rpc = RPC(freqtradebot) @@ -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"]) assert res["stake"] == "BTC" - assert len(res["currencies"]) == 1 + assert len(res["currencies"]) == 3 assert res["currencies"][0]["currency"] == "BTC" - # ETH has not been converted. - assert all(currency["currency"] != "ETH" for currency in res["currencies"]) + curr_ETH = next(currency for currency in res["currencies"] if currency["currency"] == "ETH") + # coins are part of the result, but were not converted + assert curr_ETH is not None + assert curr_ETH["currency"] == "ETH" + assert curr_ETH["est_stake"] == 0 + curr_ADA = next( + currency for currency in res["currencies"] if currency["currency"] == "ADA/USDT:USDT" + ) + assert curr_ADA is not None + assert curr_ADA["currency"] == "ADA/USDT:USDT" + # Fall back to collateral value when rate not available + assert curr_ADA["est_stake"] == 20 + + assert log_has_re(r"Error .* getting rate for futures ADA.*", caplog) + assert log_has_re(r"Error .* getting rate for ETH.*", caplog) @pytest.mark.parametrize("proxy_coin", [None, "BNFCR"])