Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions Exchange.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)."""
Expand Down Expand Up @@ -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':
Expand Down Expand Up @@ -1443,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
Expand Down