"""Binance:交易账户 futures income;资金账户 deposits/withdrawals/transfers.USDT.""" from __future__ import annotations from typing import Any, Callable, Optional from lib.account_ledger.account_ledger_normalize import ( ACCOUNT_FUNDING, ACCOUNT_TRADING, from_ccxt_ledger_entry, kind_from_raw, make_ref_id, normalize_row, ) def _paginate_income(exchange, *, start_ms: int, end_ms: int, max_pages: int = 15) -> list[dict]: out: list[dict] = [] cursor = int(start_ms) end = int(end_ms) for _ in range(max_pages): try: if hasattr(exchange, "fapiPrivateGetIncome"): batch = exchange.fapiPrivateGetIncome( {"startTime": cursor, "endTime": end, "limit": 1000} ) else: batch = exchange.fetch_ledger( "USDT", cursor, 1000, {"type": "swap", "until": end} ) # already unified return batch or [] except Exception: break if not batch: break out.extend(batch) if len(batch) < 1000: break last_t = batch[-1].get("time") or batch[-1].get("timestamp") try: last_i = int(float(last_t)) except Exception: break if last_i >= end: break cursor = last_i + 1 return out def _income_to_row(raw: dict) -> Optional[dict[str, Any]]: if not isinstance(raw, dict): return None # raw fapi income if "income" in raw or "incomeType" in raw: amt = raw.get("income") ts = raw.get("time") ccy = raw.get("asset") or "USDT" raw_type = str(raw.get("incomeType") or "") ref = str(raw.get("tranId") or raw.get("tradeId") or "") return normalize_row( account=ACCOUNT_TRADING, ccy=str(ccy), amount=amt, ts_ms=ts, ref_id=ref or make_ref_id("trading", ccy, ts, amt, raw_type), raw_type=raw_type, symbol=str(raw.get("symbol") or ""), note=str(raw.get("info") or ""), kind=kind_from_raw(raw_type, float(amt) if amt is not None else None), ) return from_ccxt_ledger_entry(raw, account=ACCOUNT_TRADING) def _dep_wd_to_row(entry: dict, *, kind: str) -> Optional[dict[str, Any]]: if not isinstance(entry, dict): return None info = entry.get("info") if isinstance(entry.get("info"), dict) else {} amount = entry.get("amount") ts = entry.get("timestamp") or info.get("insertTime") or info.get("applyTime") ccy = entry.get("currency") or info.get("coin") or "USDT" status = entry.get("status") or info.get("status") or "" ref = str(entry.get("id") or info.get("txId") or info.get("id") or "") amt = amount try: af = float(amount) if kind == "withdraw" and af > 0: af = -af amt = af except Exception: pass return normalize_row( account=ACCOUNT_FUNDING, ccy=str(ccy), amount=amt, ts_ms=ts, ref_id=ref or make_ref_id("funding", kind, ccy, ts, amount), raw_type=kind, note=str(status), kind=kind, ) def _transfer_to_row(entry: dict) -> Optional[dict[str, Any]]: if not isinstance(entry, dict): return None info = entry.get("info") if isinstance(entry.get("info"), dict) else {} amount = entry.get("amount") ts = entry.get("timestamp") or info.get("timestamp") ccy = entry.get("currency") or info.get("asset") or "USDT" ref = str(entry.get("id") or info.get("tranId") or info.get("id") or "") frm = str(entry.get("fromAccount") or info.get("from") or "") to = str(entry.get("toAccount") or info.get("to") or "") try: amt = float(amount) except Exception: return None # 资金侧视角:从资金转出为负,转入为正(粗分) note = f"{frm}->{to}".strip("->") raw_type = "transfer" return normalize_row( account=ACCOUNT_FUNDING, ccy=str(ccy), amount=amt, ts_ms=ts, ref_id=ref or make_ref_id("funding", "transfer", ccy, ts, amt), raw_type=raw_type, note=note, kind=kind_from_raw("transfer", amt), ) def fetch_binance_account_ledger( exchange, *, start_ms: int, end_ms: int, ensure_markets: Optional[Callable[[], None]] = None, ) -> tuple[list[dict[str, Any]], list[str]]: errors: list[str] = [] rows: list[dict[str, Any]] = [] if ensure_markets: try: ensure_markets() except Exception as e: errors.append(f"markets:{e}") # 交易账户 try: raw = _paginate_income(exchange, start_ms=start_ms, end_ms=end_ms) for e in raw: n = _income_to_row(e) if n and n["ccy"] == "USDT": rows.append(n) except Exception as e: errors.append(f"trading:{e}") # 资金账户:充提 + 划转 for label, fn, kind in ( ("deposits", "fetch_deposits", "deposit"), ("withdrawals", "fetch_withdrawals", "withdraw"), ): try: meth = getattr(exchange, fn, None) if not callable(meth): continue batch = meth("USDT", int(start_ms), 1000, {"until": int(end_ms)}) or [] for e in batch: n = _dep_wd_to_row(e, kind=kind) if n: rows.append(n) except Exception as e: errors.append(f"{label}:{e}") try: if hasattr(exchange, "fetch_transfers"): batch = ( exchange.fetch_transfers("USDT", int(start_ms), 1000, {"until": int(end_ms)}) or [] ) for e in batch: n = _transfer_to_row(e) if n: rows.append(n) except Exception as e: errors.append(f"transfers:{e}") return rows, errors