feat(instance): add exchange account ledger tab with SSE sync
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user