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() {
账号