feat(data_management): 优化数据管理功能与API接口
在后端 API 中新增 `_get_active_symbols_from_income` 函数,通过收益历史 API 获取有交易活动的交易对,减少后续请求数。更新 `fetch_binance_data` 函数以支持动态获取交易对,并优化前端 `DataManagement` 组件,确保仅显示状态为 active 的账号。调整 API 服务以支持可选参数 `activeOnly`,提升数据查询的灵活性与用户体验。
This commit is contained in:
parent
69be629369
commit
aaef73c2b3
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
账号
|
||||
<select value={dbAccountId} onChange={(e) => setDbAccountId(e.target.value)}>
|
||||
<option value="">请选择</option>
|
||||
{accounts.map((a) => (
|
||||
{activeAccounts.map((a) => (
|
||||
<option key={a.id} value={a.id}>{a.name || `账号${a.id}`}</option>
|
||||
))}
|
||||
</select>
|
||||
|
|
@ -200,14 +200,14 @@ export default function DataManagement() {
|
|||
账号
|
||||
<select value={bnAccountId} onChange={(e) => setBnAccountId(e.target.value)}>
|
||||
<option value="">请选择</option>
|
||||
{accounts.map((a) => (
|
||||
{activeAccounts.map((a) => (
|
||||
<option key={a.id} value={a.id}>{a.name || `账号${a.id}`}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
交易对
|
||||
<input type="text" value={bnSymbols} onChange={(e) => setBnSymbols(e.target.value)} placeholder="ASTERUSDT,FILUSDT 逗号分隔" style={{ minWidth: 220 }} />
|
||||
<input type="text" value={bnSymbols} onChange={(e) => setBnSymbols(e.target.value)} placeholder="留空=全部交易对,或逗号分隔如 ASTERUSDT,FILUSDT" style={{ minWidth: 280 }} />
|
||||
</label>
|
||||
<label>
|
||||
数据类型
|
||||
|
|
@ -217,8 +217,9 @@ export default function DataManagement() {
|
|||
</select>
|
||||
</label>
|
||||
<label>
|
||||
最近天数
|
||||
时间范围
|
||||
<select value={bnDays} onChange={(e) => setBnDays(Number(e.target.value))}>
|
||||
<option value={0}>当天</option>
|
||||
{[1, 2, 3, 5, 7].map((d) => (
|
||||
<option key={d} value={d}>{d} 天</option>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -931,8 +931,9 @@ export const api = {
|
|||
},
|
||||
|
||||
// 数据管理(管理员专用)
|
||||
getDataManagementAccounts: async () => {
|
||||
const response = await fetch(buildUrl('/api/admin/data/accounts'), { headers: withAuthHeaders() });
|
||||
getDataManagementAccounts: async (activeOnly = true) => {
|
||||
const params = activeOnly ? '?active_only=true' : '';
|
||||
const response = await fetch(buildUrl(`/api/admin/data/accounts${params}`), { headers: withAuthHeaders() });
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ detail: '获取账号列表失败' }));
|
||||
throw new Error(error.detail || '获取账号列表失败');
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user