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
|
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")
|
@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()
|
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 [])]
|
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}
|
return {"accounts": accounts}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -114,9 +149,9 @@ async def query_db_trades(
|
||||||
async def fetch_binance_data(
|
async def fetch_binance_data(
|
||||||
_admin=Depends(get_admin_user),
|
_admin=Depends(get_admin_user),
|
||||||
account_id: int = Query(..., ge=1),
|
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"),
|
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)
|
从币安拉取订单/成交记录(需账号已配置 API)
|
||||||
|
|
@ -141,15 +176,26 @@ async def fetch_binance_data(
|
||||||
raise HTTPException(status_code=502, detail=f"连接币安失败: {e}")
|
raise HTTPException(status_code=502, detail=f"连接币安失败: {e}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
sym_list = [s.strip().upper() for s in symbols.split(",") if s.strip()]
|
now = datetime.now(BEIJING_TZ)
|
||||||
if not sym_list:
|
end_ms = int(now.timestamp() * 1000)
|
||||||
raise HTTPException(status_code=400, detail="请指定至少一个交易对")
|
if days == 0:
|
||||||
|
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||||
end_ms = int(datetime.now(BEIJING_TZ).timestamp() * 1000)
|
start_ms = int(today_start.timestamp() * 1000)
|
||||||
|
else:
|
||||||
start_ms = end_ms - days * 24 * 3600 * 1000
|
start_ms = end_ms - days * 24 * 3600 * 1000
|
||||||
|
|
||||||
all_data = []
|
sym_list = [s.strip().upper() for s in (symbols or "").split(",") if s.strip()]
|
||||||
for sym in sym_list:
|
if not sym_list:
|
||||||
|
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="无法获取交易对列表,请手动指定交易对")
|
||||||
|
|
||||||
|
sem = asyncio.Semaphore(5)
|
||||||
|
|
||||||
|
async def _fetch_one(sym: str):
|
||||||
|
async with sem:
|
||||||
try:
|
try:
|
||||||
if data_type == "trades":
|
if data_type == "trades":
|
||||||
rows = await client.client.futures_account_trades(
|
rows = await client.client.futures_account_trades(
|
||||||
|
|
@ -170,15 +216,27 @@ async def fetch_binance_data(
|
||||||
if isinstance(rows, list):
|
if isinstance(rows, list):
|
||||||
for r in rows:
|
for r in rows:
|
||||||
r["_symbol"] = sym
|
r["_symbol"] = sym
|
||||||
all_data.extend(rows)
|
return rows
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
all_data.append({"_symbol": sym, "_error": str(e)})
|
return [{"_symbol": sym, "_error": str(e)}]
|
||||||
await asyncio.sleep(0.2)
|
finally:
|
||||||
|
await asyncio.sleep(0.12)
|
||||||
|
|
||||||
|
tasks = [_fetch_one(sym) for sym in sym_list]
|
||||||
|
chunks = await asyncio.gather(*tasks)
|
||||||
|
all_data = []
|
||||||
|
for ch in chunks:
|
||||||
|
all_data.extend(ch)
|
||||||
|
|
||||||
time_key = "time" if (all_data and "time" in (all_data[0] or {})) else "updateTime"
|
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)
|
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:
|
finally:
|
||||||
if client.client:
|
if client.client:
|
||||||
await client.client.close_connection()
|
await client.client.close_connection()
|
||||||
|
|
|
||||||
|
|
@ -34,12 +34,16 @@ export default function DataManagement() {
|
||||||
const [bnResult, setBnResult] = useState(null)
|
const [bnResult, setBnResult] = useState(null)
|
||||||
const [bnError, setBnError] = useState('')
|
const [bnError, setBnError] = useState('')
|
||||||
|
|
||||||
|
const activeAccounts = accounts.filter((a) => (a.status || 'active').toLowerCase() === 'active')
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
api.getDataManagementAccounts().then((r) => {
|
api.getDataManagementAccounts().then((r) => {
|
||||||
const list = Array.isArray(r?.accounts) ? r.accounts : (Array.isArray(r) ? r : [])
|
const list = Array.isArray(r?.accounts) ? r.accounts : (Array.isArray(r) ? r : [])
|
||||||
setAccounts(list)
|
setAccounts(list)
|
||||||
if (!dbAccountId && list.length) setDbAccountId(String(list[0].id))
|
const active = list.filter((a) => (a.status || 'active').toLowerCase() === 'active')
|
||||||
if (!bnAccountId && list.length) setBnAccountId(String(list[0].id))
|
const first = active[0] || list[0]
|
||||||
|
if (!dbAccountId && first) setDbAccountId(String(first.id))
|
||||||
|
if (!bnAccountId && first) setBnAccountId(String(first.id))
|
||||||
}).catch(() => {})
|
}).catch(() => {})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
|
@ -68,21 +72,17 @@ export default function DataManagement() {
|
||||||
|
|
||||||
const fetchBinance = async () => {
|
const fetchBinance = async () => {
|
||||||
const aid = parseInt(bnAccountId, 10)
|
const aid = parseInt(bnAccountId, 10)
|
||||||
const syms = bnSymbols.trim()
|
if (!aid) {
|
||||||
if (!aid || !syms) {
|
setBnError('请选择账号')
|
||||||
setBnError('请选择账号并输入交易对')
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setBnLoading(true)
|
setBnLoading(true)
|
||||||
setBnError('')
|
setBnError('')
|
||||||
setBnResult(null)
|
setBnResult(null)
|
||||||
try {
|
try {
|
||||||
const res = await api.postDataManagementFetchBinance({
|
const params = { account_id: aid, data_type: bnDataType, days: bnDays }
|
||||||
account_id: aid,
|
if (bnSymbols.trim()) params.symbols = bnSymbols.trim()
|
||||||
symbols: syms,
|
const res = await api.postDataManagementFetchBinance(params)
|
||||||
data_type: bnDataType,
|
|
||||||
days: bnDays,
|
|
||||||
})
|
|
||||||
setBnResult(res)
|
setBnResult(res)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setBnError(e?.message || '拉取失败')
|
setBnError(e?.message || '拉取失败')
|
||||||
|
|
@ -119,7 +119,7 @@ export default function DataManagement() {
|
||||||
账号
|
账号
|
||||||
<select value={dbAccountId} onChange={(e) => setDbAccountId(e.target.value)}>
|
<select value={dbAccountId} onChange={(e) => setDbAccountId(e.target.value)}>
|
||||||
<option value="">请选择</option>
|
<option value="">请选择</option>
|
||||||
{accounts.map((a) => (
|
{activeAccounts.map((a) => (
|
||||||
<option key={a.id} value={a.id}>{a.name || `账号${a.id}`}</option>
|
<option key={a.id} value={a.id}>{a.name || `账号${a.id}`}</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
|
@ -200,14 +200,14 @@ export default function DataManagement() {
|
||||||
账号
|
账号
|
||||||
<select value={bnAccountId} onChange={(e) => setBnAccountId(e.target.value)}>
|
<select value={bnAccountId} onChange={(e) => setBnAccountId(e.target.value)}>
|
||||||
<option value="">请选择</option>
|
<option value="">请选择</option>
|
||||||
{accounts.map((a) => (
|
{activeAccounts.map((a) => (
|
||||||
<option key={a.id} value={a.id}>{a.name || `账号${a.id}`}</option>
|
<option key={a.id} value={a.id}>{a.name || `账号${a.id}`}</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<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>
|
||||||
<label>
|
<label>
|
||||||
数据类型
|
数据类型
|
||||||
|
|
@ -217,8 +217,9 @@ export default function DataManagement() {
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
最近天数
|
时间范围
|
||||||
<select value={bnDays} onChange={(e) => setBnDays(Number(e.target.value))}>
|
<select value={bnDays} onChange={(e) => setBnDays(Number(e.target.value))}>
|
||||||
|
<option value={0}>当天</option>
|
||||||
{[1, 2, 3, 5, 7].map((d) => (
|
{[1, 2, 3, 5, 7].map((d) => (
|
||||||
<option key={d} value={d}>{d} 天</option>
|
<option key={d} value={d}>{d} 天</option>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -931,8 +931,9 @@ export const api = {
|
||||||
},
|
},
|
||||||
|
|
||||||
// 数据管理(管理员专用)
|
// 数据管理(管理员专用)
|
||||||
getDataManagementAccounts: async () => {
|
getDataManagementAccounts: async (activeOnly = true) => {
|
||||||
const response = await fetch(buildUrl('/api/admin/data/accounts'), { headers: withAuthHeaders() });
|
const params = activeOnly ? '?active_only=true' : '';
|
||||||
|
const response = await fetch(buildUrl(`/api/admin/data/accounts${params}`), { headers: withAuthHeaders() });
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const error = await response.json().catch(() => ({ detail: '获取账号列表失败' }));
|
const error = await response.json().catch(() => ({ detail: '获取账号列表失败' }));
|
||||||
throw new Error(error.detail || '获取账号列表失败');
|
throw new Error(error.detail || '获取账号列表失败');
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user