From 0a33e15ae07e0849277fd57b2dd7ef8173ac0569 Mon Sep 17 00:00:00 2001 From: Alexch03 Date: Mon, 15 Jun 2026 01:49:20 +0200 Subject: [PATCH 1/2] Show correct balance for Bitget UTA / Elite (copy-trading) accounts Exchange.fetch_balance() read the Bitget balance only from the classic v2 shape (info[0]["available"]). A UTA / Elite (copy-trading) API key runs on Bitget's v3 API, so fetch_balance raised "40731 This product does not support copy trading" and the GUI could not show the account's balance. connect() now auto-detects a UTA/Elite account (probes v3 account-assets; classic keys return 40084 and are unaffected) and enables ccxt's v3 routing via options["uta"]. fetch_balance() reads the UTA balance from the unified total. Classic accounts are unchanged. Tested live: a classic key still returns its v2 balance; an elite/UTA key now returns its balance instead of erroring. --- Exchange.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/Exchange.py b/Exchange.py index 0bc813a7..ce76158a 100644 --- a/Exchange.py +++ b/Exchange.py @@ -284,6 +284,8 @@ def __init__(self, id: str, user: User = None): self.swap = [] self._user = user self.error = None + # Bitget: None = not yet detected, True/False = UTA(Elite) vs classic account + self._bitget_uta = None # _log removed: Exchange module uses `logging_helpers.human_log` directly @@ -358,6 +360,22 @@ def connect(self): self.error = None except Exception as exc: self.error = str(exc) + # Bitget: auto-detect UTA / Elite (copy-trading) accounts so balances, + # positions and orders are read from the v3 API. Classic accounts return + # error 40084 on the v3 probe and keep the v2 path (no behaviour change). + if self.id == "bitget" and self._user and self.user.key != 'key' and not self.error: + self._bitget_uta = False + try: + self.instance.private_uta_get_v3_account_assets() + self._bitget_uta = True + except Exception as exc: + # 40084 == "Classic Account mode": expected for normal keys. + if "40084" not in str(exc): + self._bitget_uta = False + try: + self.instance.options["uta"] = self._bitget_uta + except Exception: + pass def close(self): """Close the exchange instance and release resources (e.g. aiohttp sessions).""" @@ -1020,6 +1038,16 @@ def fetch_balance(self, market_type: str, symbol : str = None): if self.id == "hyperliquid": return float(balance["total"]["USDC"]) if self.id == "bitget": + if getattr(self, "_bitget_uta", False): + # UTA / Elite (copy-trading) account: v3 balance has no marginCoin + # list; use the ccxt-unified total (wallet balance). + try: + return float(balance["USDT"]["total"]) + except Exception: + for x in (balance.get("info") or []): + if isinstance(x, dict) and x.get("coin") == "USDT": + return float(x.get("balance") or x.get("available") or 0.0) + return 0.0 return float(balance["info"][0]["available"]) elif self.id == "bybit": if market_type == 'swap': From 9e9a7bb14aedd543fffaaf1dc2a171575db23053 Mon Sep 17 00:00:00 2001 From: Alexch03 Date: Mon, 15 Jun 2026 02:12:21 +0200 Subject: [PATCH 2/2] Fix Bitget UTA/Elite fetch_history (income from v3 trade/fills) fetch_history used fetch_ledger, which ccxt does not route to the v3 API for UTA accounts, so it raised "40731 This product does not support copy trading". For UTA/Elite accounts, build the income history from the v3 trade/fills endpoint (execPnl minus fees), matching the trade/fee filter of the classic v2 path. Classic accounts are unchanged. --- Exchange.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/Exchange.py b/Exchange.py index ce76158a..8a663bf7 100644 --- a/Exchange.py +++ b/Exchange.py @@ -1471,6 +1471,48 @@ def fetch_history(self, since: int = None): else: self.save_income_other(history, self.user.name) elif self.id == "bitget": + if getattr(self, "_bitget_uta", False): + # UTA/Elite: classic fetch_ledger (v2) is rejected (40731); build + # income from v3 trade fills (execPnl + fees), matching the v2 + # trade/fee filter used by the classic path below. + now = self.instance.milliseconds() + if not since: + since = now - 365 * 24 * 60 * 60 * 1000 + params = {"category": "USDT-FUTURES", "limit": "100"} + seen = set() + end = now + while True: + params["endTime"] = int(end) + params["startTime"] = int(since) + fetched = self.instance.private_uta_get_v3_trade_fills(params) + rows = (fetched.get("data") or {}).get("list") or [] + if not rows: + break + new = 0 + for x in rows: + eid = x.get("execId") + if not eid or eid in seen: + continue + seen.add(eid) + new += 1 + fee = 0.0 + for fd in (x.get("feeDetail") or []): + try: + fee += float(fd.get("fee") or 0) + except Exception: + pass + all.append({ + "symbol": x.get("symbol"), + "timestamp": int(x.get("createdTime") or 0), + # v3 feeDetail.fee is a positive cost -> subtract it. + "income": float(x.get("execPnl") or 0) - fee, + "uniqueid": eid, + }) + oldest = int(rows[-1].get("createdTime") or 0) + if new == 0 or oldest <= since: + break + end = oldest - 1 + return all day = 24 * 60 * 60 * 1000 week = 7 * day max = 365 * day