From aaef73c2b34267587e96ac33efdde0606314eae1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=96=87=E8=96=87=E5=AE=89?= Date: Sun, 22 Feb 2026 10:43:37 +0800 Subject: [PATCH] =?UTF-8?q?feat(data=5Fmanagement):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E7=AE=A1=E7=90=86=E5=8A=9F=E8=83=BD=E4=B8=8E?= =?UTF-8?q?API=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在后端 API 中新增 `_get_active_symbols_from_income` 函数,通过收益历史 API 获取有交易活动的交易对,减少后续请求数。更新 `fetch_binance_data` 函数以支持动态获取交易对,并优化前端 `DataManagement` 组件,确保仅显示状态为 active 的账号。调整 API 服务以支持可选参数 `activeOnly`,提升数据查询的灵活性与用户体验。 --- backend/api/routes/data_management.py | 126 +++++++++++++++------ frontend/src/components/DataManagement.jsx | 31 ++--- frontend/src/services/api.js | 5 +- 3 files changed, 111 insertions(+), 51 deletions(-) diff --git a/backend/api/routes/data_management.py b/backend/api/routes/data_management.py index 953b681..2e1cd9c 100644 --- a/backend/api/routes/data_management.py +++ b/backend/api/routes/data_management.py @@ -60,11 +60,46 @@ def _get_timestamp_range(period: Optional[str], start_date: Optional[str], end_d return start_ts, end_ts +async def _get_active_symbols_from_income(binance_client, start_ms: int, end_ms: int) -> list: + """ + 通过收益历史 API 获取该时间段内有交易活动的交易对,避免全量遍历 250+ 交易对。 + 一次 API 调用(weight 100)即可拿到有成交/盈亏的 symbol 列表,大幅减少后续 trades/orders 的请求数。 + """ + try: + symbols = set() + current_end = end_ms + for _ in range(10): # 最多分页 10 次(单次最多 1000 条) + rows = await binance_client.futures_income_history( + startTime=start_ms, + endTime=current_end, + limit=1000, + recvWindow=20000, + ) + if not rows: + break + for r in rows: + sym = (r.get("symbol") or "").strip() + if sym and sym.endswith("USDT"): + symbols.add(sym) + if len(rows) < 1000: + break + oldest = min(r.get("time", current_end) for r in rows) + current_end = oldest - 1 + if current_end < start_ms: + break + await asyncio.sleep(0.15) + return sorted(symbols) + except Exception: + return [] + + @router.get("/accounts") -async def list_accounts(_admin=Depends(get_admin_user)): - """获取所有账号列表,供数据管理选择""" +async def list_accounts(_admin=Depends(get_admin_user), active_only: bool = Query(False)): + """获取账号列表,供数据管理选择。active_only=true 时仅返回 status=active 的账号""" rows = Account.list_all() accounts = [{"id": r["id"], "name": r.get("name") or f"Account {r['id']}", "status": r.get("status") or "active"} for r in (rows or [])] + if active_only: + accounts = [a for a in accounts if (a.get("status") or "").lower() == "active"] return {"accounts": accounts} @@ -114,9 +149,9 @@ async def query_db_trades( async def fetch_binance_data( _admin=Depends(get_admin_user), account_id: int = Query(..., ge=1), - symbols: str = Query(..., description="交易对,逗号分隔,如 ASTERUSDT,FILUSDT"), + symbols: Optional[str] = Query(None, description="交易对,逗号分隔;留空则拉取该时间段内全部交易对的订单/成交"), data_type: str = Query("trades", description="orders 或 trades"), - days: int = Query(7, ge=1, le=7), + days: int = Query(7, ge=0, le=7), ): """ 从币安拉取订单/成交记录(需账号已配置 API) @@ -141,44 +176,67 @@ async def fetch_binance_data( raise HTTPException(status_code=502, detail=f"连接币安失败: {e}") try: - sym_list = [s.strip().upper() for s in symbols.split(",") if s.strip()] + now = datetime.now(BEIJING_TZ) + end_ms = int(now.timestamp() * 1000) + if days == 0: + today_start = now.replace(hour=0, minute=0, second=0, microsecond=0) + start_ms = int(today_start.timestamp() * 1000) + else: + start_ms = end_ms - days * 24 * 3600 * 1000 + + sym_list = [s.strip().upper() for s in (symbols or "").split(",") if s.strip()] if not sym_list: - raise HTTPException(status_code=400, detail="请指定至少一个交易对") + sym_list = await _get_active_symbols_from_income(client.client, start_ms, end_ms) + if not sym_list: + sym_list = await client.get_all_usdt_pairs() + if not sym_list: + raise HTTPException(status_code=500, detail="无法获取交易对列表,请手动指定交易对") - end_ms = int(datetime.now(BEIJING_TZ).timestamp() * 1000) - start_ms = end_ms - days * 24 * 3600 * 1000 + sem = asyncio.Semaphore(5) + async def _fetch_one(sym: str): + async with sem: + try: + if data_type == "trades": + rows = await client.client.futures_account_trades( + symbol=sym, + startTime=start_ms, + endTime=end_ms, + limit=1000, + recvWindow=20000, + ) + else: + rows = await client.client.futures_get_all_orders( + symbol=sym, + startTime=start_ms, + endTime=end_ms, + limit=1000, + recvWindow=20000, + ) + if isinstance(rows, list): + for r in rows: + r["_symbol"] = sym + return rows + except Exception as e: + return [{"_symbol": sym, "_error": str(e)}] + finally: + await asyncio.sleep(0.12) + + tasks = [_fetch_one(sym) for sym in sym_list] + chunks = await asyncio.gather(*tasks) all_data = [] - for sym in sym_list: - try: - if data_type == "trades": - rows = await client.client.futures_account_trades( - symbol=sym, - startTime=start_ms, - endTime=end_ms, - limit=1000, - recvWindow=20000, - ) - else: - rows = await client.client.futures_get_all_orders( - symbol=sym, - startTime=start_ms, - endTime=end_ms, - limit=1000, - recvWindow=20000, - ) - if isinstance(rows, list): - for r in rows: - r["_symbol"] = sym - all_data.extend(rows) - except Exception as e: - all_data.append({"_symbol": sym, "_error": str(e)}) - await asyncio.sleep(0.2) + for ch in chunks: + all_data.extend(ch) time_key = "time" if (all_data and "time" in (all_data[0] or {})) else "updateTime" all_data.sort(key=lambda x: x.get(time_key, 0), reverse=True) - return {"total": len(all_data), "data_type": data_type, "data": all_data} + return { + "total": len(all_data), + "data_type": data_type, + "symbols_queried": len(sym_list), + "data": all_data, + } finally: if client.client: await client.client.close_connection() diff --git a/frontend/src/components/DataManagement.jsx b/frontend/src/components/DataManagement.jsx index 4f0d1f8..a20cf39 100644 --- a/frontend/src/components/DataManagement.jsx +++ b/frontend/src/components/DataManagement.jsx @@ -34,12 +34,16 @@ export default function DataManagement() { const [bnResult, setBnResult] = useState(null) const [bnError, setBnError] = useState('') + const activeAccounts = accounts.filter((a) => (a.status || 'active').toLowerCase() === 'active') + useEffect(() => { api.getDataManagementAccounts().then((r) => { const list = Array.isArray(r?.accounts) ? r.accounts : (Array.isArray(r) ? r : []) setAccounts(list) - if (!dbAccountId && list.length) setDbAccountId(String(list[0].id)) - if (!bnAccountId && list.length) setBnAccountId(String(list[0].id)) + const active = list.filter((a) => (a.status || 'active').toLowerCase() === 'active') + const first = active[0] || list[0] + if (!dbAccountId && first) setDbAccountId(String(first.id)) + if (!bnAccountId && first) setBnAccountId(String(first.id)) }).catch(() => {}) }, []) @@ -68,21 +72,17 @@ export default function DataManagement() { const fetchBinance = async () => { const aid = parseInt(bnAccountId, 10) - const syms = bnSymbols.trim() - if (!aid || !syms) { - setBnError('请选择账号并输入交易对') + if (!aid) { + setBnError('请选择账号') return } setBnLoading(true) setBnError('') setBnResult(null) try { - const res = await api.postDataManagementFetchBinance({ - account_id: aid, - symbols: syms, - data_type: bnDataType, - days: bnDays, - }) + const params = { account_id: aid, data_type: bnDataType, days: bnDays } + if (bnSymbols.trim()) params.symbols = bnSymbols.trim() + const res = await api.postDataManagementFetchBinance(params) setBnResult(res) } catch (e) { setBnError(e?.message || '拉取失败') @@ -119,7 +119,7 @@ export default function DataManagement() { 账号 @@ -200,14 +200,14 @@ export default function DataManagement() { 账号