Wallets._update_live drops cross-margin positions: collateral==0 filter runs before the initialMargin fallback
Describe your environment
- Operating system: macOS 15.2 (arm64)
- Python Version: 3.12.3
- CCXT version: 4.5.82
- Freqtrade Version: 2026.8
Describe the problem
In Wallets._update_live, open positions are discarded when ccxt reports
collateral == 0.0 — but the very next lines already know how to substitute
that value with initialMargin. The filter runs before the fallback:
# freqtrade/wallets.py
204: if position["side"] is None or position["collateral"] == 0.0:
205: continue
...
208: collateral = safe_value_fallback(position, "initialMargin", "collateral", 0.0)On Binance USDⓈ-M cross margin, liquidationPrice is frequently None.
ccxt derives collateral from liquidationPrice, so it returns 0.0 for
those positions — while initialMargin is present and correct.
The result is that genuinely open positions are silently dropped from
Wallets._positions.
Observed data
Captured via exchange.fetch_positions() — the same call _update_live
makes — against a live Binance USDⓈ-M cross-margin account:
| symbol | side | collateral | initialMargin | liquidationPrice | notional |
|---|---|---|---|---|---|
| PAIR_A/USDT:USDT | long | 0.0 | 3.815 | None |
76.30 |
| PAIR_B/USDT:USDT | long | 0.0 | 40.878 | None |
163.51 |
(Pair names replaced with placeholders. All numeric values are the real captured data, unmodified.)
Both positions are open and carry real margin. Both are skipped at line 204.
A third position in the same account, which did have a liquidationPrice,
reported a non-zero collateral and was kept — so the behaviour is
per-position, not per-account.
Impact
1. Wallets._positions is incomplete in live cross-margin.
Consumers such as get_collateral(), _check_exit_amount() and the
/balance RPC see only the subset of positions for which ccxt happened to
derive a collateral value.
2. Wallet.total for the stake currency is wrong on exchanges with
balance_includes_unrealized_pnl: True (Binance futures).
_strip_unrealized_pnl sums unrealized PnL over the already filtered
dict:
238: upnl = sum(pos.unrealized_pnl for pos in positions.values())
241: wallets[...] = stake_wallet._replace(total=stake_wallet.total - upnl)Positions dropped at line 204 contribute no unrealized_pnl, so only part
of the account's unrealized PnL is stripped. In the account above both
open positions were skipped, so 100% of the account's unrealized PnL was
excluded from the correction — total was left uncorrected by -75.99
USDT at the moment of capture, and get_total() reported that much more
equity than the account actually had.
Suggested fix
Apply the initialMargin fallback before the emptiness test, so that a
position is only skipped when it genuinely has no margin:
for position in positions:
symbol = position["symbol"]
collateral = safe_value_fallback(position, "initialMargin", "collateral", 0.0)
if position["side"] is None or collateral == 0.0:
continue
...This keeps the existing intent ("skip positions that are not open") while no longer relying on a field that Binance does not always allow ccxt to derive.
Steps to reproduce
- Hold an open position on Binance USDⓈ-M in cross margin mode for which the exchange returns no per-position liquidation price.
- Call
exchange.fetch_positions()and inspectcollateralvsinitialMargin. - Observe that
Wallets._positionsomits the position.
Source: freqtrade/freqtrade