feat(data_management): 添加数据管理功能与接口

在后端 API 中新增数据管理路由,支持从币安拉取订单和成交记录的功能。前端应用中引入数据管理组件,并在路由中添加相应的链接。更新了 API 服务,提供获取账号列表和查询 DB 交易的接口,增强了系统的数据处理能力与用户体验。
This commit is contained in:
薇薇安 2026-02-22 10:05:18 +08:00
parent fa7208f5f3
commit f2b04911a2
8 changed files with 815 additions and 2 deletions

View File

@ -3,7 +3,7 @@ FastAPI应用主入口
"""
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from api.routes import config, trades, stats, dashboard, account, recommendations, system, accounts, auth, admin, public
from api.routes import config, trades, stats, dashboard, account, recommendations, system, accounts, auth, admin, public, data_management
import os
import logging
from pathlib import Path
@ -228,6 +228,7 @@ app.include_router(dashboard.router, prefix="/api/dashboard", tags=["仪表板"]
app.include_router(account.router, prefix="/api/account", tags=["账户数据"])
app.include_router(recommendations.router, tags=["交易推荐"])
app.include_router(system.router, tags=["系统控制"])
app.include_router(data_management.router)
app.include_router(public.router)

View File

@ -0,0 +1,183 @@
"""
数据管理查询 DB 交易从币安拉取订单/成交供策略分析与导出
仅管理员可用
"""
import asyncio
from pathlib import Path
from fastapi import APIRouter, Query, Depends, HTTPException
from typing import Optional
from api.auth_deps import get_admin_user
from database.models import Trade, Account
from datetime import datetime, timezone, timedelta
router = APIRouter(prefix="/api/admin/data", tags=["数据管理"])
BEIJING_TZ = timezone(timedelta(hours=8))
def _get_timestamp_range(period: Optional[str], start_date: Optional[str], end_date: Optional[str]):
now = datetime.now(BEIJING_TZ)
end_ts = int(now.timestamp())
start_ts = None
if period:
if period == "today":
today = now.replace(hour=0, minute=0, second=0, microsecond=0)
start_ts = int(today.timestamp())
elif period == "1d":
start_ts = end_ts - 24 * 3600
elif period == "7d":
start_ts = end_ts - 7 * 24 * 3600
elif period == "30d":
start_ts = end_ts - 30 * 24 * 3600
elif period == "week":
days = now.weekday()
week_start = (now - timedelta(days=days)).replace(hour=0, minute=0, second=0, microsecond=0)
start_ts = int(week_start.timestamp())
elif period == "month":
month_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
start_ts = int(month_start.timestamp())
if start_date:
try:
s = start_date if len(start_date) > 10 else f"{start_date} 00:00:00"
dt = datetime.strptime(s, "%Y-%m-%d %H:%M:%S").replace(tzinfo=BEIJING_TZ)
start_ts = int(dt.timestamp())
except ValueError:
pass
if end_date:
try:
s = end_date if len(end_date) > 10 else f"{end_date} 23:59:59"
dt = datetime.strptime(s, "%Y-%m-%d %H:%M:%S").replace(tzinfo=BEIJING_TZ)
end_ts = int(dt.timestamp())
except ValueError:
pass
if start_ts is None:
start_ts = end_ts - 7 * 24 * 3600 # 默认 7 天
return start_ts, end_ts
@router.get("/accounts")
async def list_accounts(_admin=Depends(get_admin_user)):
"""获取所有账号列表,供数据管理选择"""
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 [])]
return {"accounts": accounts}
@router.get("/trades")
async def query_db_trades(
_admin=Depends(get_admin_user),
account_id: int = Query(..., ge=1, description="账号 ID"),
period: Optional[str] = Query(None, description="today/1d/7d/30d/week/month"),
date: Optional[str] = Query(None, description="YYYY-MM-DD指定日期等同于 start_date=end_date"),
start_date: Optional[str] = Query(None),
end_date: Optional[str] = Query(None),
symbol: Optional[str] = Query(None),
time_filter: str = Query("created", description="created/entry/exit"),
reconciled_only: bool = Query(False),
limit: int = Query(500, ge=1, le=2000),
):
"""
查询 DB 交易记录管理员可指定任意账号
"""
sd, ed = start_date, end_date
if date:
sd, ed = date, date
start_ts, end_ts = _get_timestamp_range(period or "today", sd, ed)
trades = Trade.get_all(
start_timestamp=start_ts,
end_timestamp=end_ts,
symbol=symbol,
status=None,
account_id=account_id,
time_filter=time_filter,
limit=limit,
reconciled_only=reconciled_only,
include_sync=True,
)
out = []
for t in trades:
row = dict(t)
for k, v in row.items():
if hasattr(v, "isoformat"):
row[k] = v.isoformat()
out.append(row)
return {"total": len(out), "trades": out}
@router.post("/binance-fetch")
async def fetch_binance_data(
_admin=Depends(get_admin_user),
account_id: int = Query(..., ge=1),
symbols: str = Query(..., description="交易对,逗号分隔,如 ASTERUSDT,FILUSDT"),
data_type: str = Query("trades", description="orders 或 trades"),
days: int = Query(7, ge=1, le=7),
):
"""
从币安拉取订单/成交记录需账号已配置 API
"""
try:
import sys
proj = Path(__file__).resolve().parents[3] # backend/api/routes -> project root
if str(proj) not in sys.path:
sys.path.insert(0, str(proj))
from trading_system.binance_client import BinanceClient
except ImportError as e:
raise HTTPException(status_code=500, detail=f"导入失败: {e}")
api_key, api_secret, use_testnet, _ = Account.get_credentials(account_id)
if not api_key or not api_secret:
raise HTTPException(status_code=400, detail="该账号未配置 API 密钥")
client = BinanceClient(api_key=api_key, api_secret=api_secret, testnet=use_testnet)
try:
await client.connect()
except Exception as e:
raise HTTPException(status_code=502, detail=f"连接币安失败: {e}")
try:
sym_list = [s.strip().upper() for s in symbols.split(",") if s.strip()]
if not sym_list:
raise HTTPException(status_code=400, detail="请指定至少一个交易对")
end_ms = int(datetime.now(BEIJING_TZ).timestamp() * 1000)
start_ms = end_ms - days * 24 * 3600 * 1000
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)
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}
finally:
if client.client:
await client.client.close_connection()

View File

@ -93,7 +93,31 @@ grep "SYS_1737500000_abcd" logs/binance_order_events.log
---
## 三、对账流程建议
## 三、从币安拉取订单/成交DB 缺失时)
当 DB 记录查不到或需直接从币安做策略分析时,可用脚本拉取:
```bash
# 拉取最近 7 天成交记录(默认,适合策略分析)
python scripts/fetch_binance_orders.py --account 2 --symbol BTCUSDT
# 多个交易对
python scripts/fetch_binance_orders.py --account 2 --symbols ASTERUSDT,FILUSDT,PENGUUSDT
# 拉取订单列表
python scripts/fetch_binance_orders.py --account 2 --symbol BTCUSDT --type orders
# 指定天数、导出
python scripts/fetch_binance_orders.py --account 2 --symbol BTCUSDT --days 7 -o binance_trades.json
```
- `--type trades`:成交记录(含价格、数量、盈亏,策略分析推荐)
- `--type orders`:订单列表(含 FILLED/CANCELED
- 币安单次时间范围最多 7 天
---
## 四、对账流程建议
1. **查 DB 今日记录**`python scripts/query_trades_today.py -o db_today.json`
2. **查币安推送日志**`tail -f logs/binance_order_events.log` 或 `grep "ORDER_TRADE_UPDATE" logs/binance_order_events.log`

View File

@ -8,6 +8,7 @@ import StatsDashboard from './components/StatsDashboard'
import AdminDashboard from './components/AdminDashboard.jsx'
import Recommendations from './components/Recommendations'
import LogMonitor from './components/LogMonitor'
import DataManagement from './components/DataManagement'
import AccountSelector from './components/AccountSelector'
import GlobalConfig from './components/GlobalConfig'
import Login from './components/Login'
@ -82,6 +83,7 @@ function App() {
{isAdmin && (
<>
<Link to="/global-config">全局配置</Link>
<Link to="/data-management">数据管理</Link>
<Link to="/logs">日志监控</Link>
</>
)}
@ -115,6 +117,7 @@ function App() {
<Route path="/config/guide" element={<ConfigGuide />} />
<Route path="/trades" element={<TradeList />} />
<Route path="/global-config" element={isAdmin ? <GlobalConfig /> : <div style={{ padding: '24px' }}>无权限</div>} />
<Route path="/data-management" element={isAdmin ? <DataManagement /> : <div style={{ padding: '24px' }}>无权限</div>} />
<Route path="/logs" element={isAdmin ? <LogMonitor /> : <div style={{ padding: '24px' }}>无权限</div>} />
</Routes>
</main>

View File

@ -0,0 +1,145 @@
.data-management {
display: flex;
flex-direction: column;
gap: 24px;
padding: 16px;
}
.data-management h2 {
margin: 0;
}
.dm-subtitle {
color: #666;
font-size: 14px;
margin: 4px 0 0 0;
}
.dm-section {
border: 1px solid #eee;
border-radius: 10px;
background: #fff;
padding: 16px;
}
.dm-section h3 {
margin: 0 0 12px 0;
font-size: 16px;
}
.dm-controls {
display: flex;
flex-wrap: wrap;
gap: 12px;
align-items: flex-end;
}
.dm-controls label {
display: flex;
flex-direction: column;
gap: 4px;
font-size: 12px;
color: #555;
}
.dm-controls label input[type="text"],
.dm-controls label input[type="date"],
.dm-controls label select {
padding: 6px 10px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 13px;
}
.dm-check {
flex-direction: row !important;
align-items: center;
}
.dm-check input {
margin-right: 6px;
}
.dm-error {
color: #c00;
margin-top: 8px;
font-size: 13px;
}
.dm-result {
margin-top: 16px;
}
.dm-result-meta {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 8px;
font-size: 13px;
color: #555;
}
.dm-table-wrap {
overflow-x: auto;
max-height: 400px;
overflow-y: auto;
}
.dm-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.dm-table th,
.dm-table td {
padding: 8px 10px;
border: 1px solid #eee;
text-align: left;
}
.dm-table th {
background: #f8f8f8;
font-weight: 600;
position: sticky;
top: 0;
}
.dm-table tbody tr:hover {
background: #f9f9f9;
}
.dm-more {
padding: 8px 10px;
font-size: 12px;
color: #666;
}
.btn {
padding: 8px 14px;
border-radius: 6px;
border: 1px solid #ddd;
background: #fff;
cursor: pointer;
font-size: 13px;
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-primary {
background: #0066cc;
color: #fff;
border-color: #0066cc;
}
.btn-primary:hover:not(:disabled) {
background: #0052a3;
}
.btn-sm {
padding: 4px 10px;
font-size: 12px;
}

View File

@ -0,0 +1,277 @@
import React, { useEffect, useState } from 'react'
import { api } from '../services/api'
import './DataManagement.css'
function downloadJson(data, filename) {
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = filename
a.click()
URL.revokeObjectURL(url)
}
export default function DataManagement() {
const [accounts, setAccounts] = useState([])
// DB
const [dbAccountId, setDbAccountId] = useState('')
const [dbDate, setDbDate] = useState('')
const [dbTimeFilter, setDbTimeFilter] = useState('created')
const [dbReconciledOnly, setDbReconciledOnly] = useState(false)
const [dbSymbol, setDbSymbol] = useState('')
const [dbLoading, setDbLoading] = useState(false)
const [dbResult, setDbResult] = useState(null)
const [dbError, setDbError] = useState('')
//
const [bnAccountId, setBnAccountId] = useState('')
const [bnSymbols, setBnSymbols] = useState('')
const [bnDataType, setBnDataType] = useState('trades')
const [bnDays, setBnDays] = useState(7)
const [bnLoading, setBnLoading] = useState(false)
const [bnResult, setBnResult] = useState(null)
const [bnError, setBnError] = useState('')
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))
}).catch(() => {})
}, [])
const queryDbTrades = async () => {
const aid = parseInt(dbAccountId, 10)
if (!aid) {
setDbError('请选择账号')
return
}
setDbLoading(true)
setDbError('')
setDbResult(null)
try {
const params = {
account_id: aid,
date: dbDate || undefined,
time_filter: dbTimeFilter,
reconciled_only: dbReconciledOnly ? 'true' : undefined,
symbol: dbSymbol || undefined,
}
const res = await api.getDataManagementDbTrades(params)
setDbResult(res)
} catch (e) {
setDbError(e?.message || '查询失败')
} finally {
setDbLoading(false)
}
}
const fetchBinance = async () => {
const aid = parseInt(bnAccountId, 10)
const syms = bnSymbols.trim()
if (!aid || !syms) {
setBnError('请选择账号并输入交易对')
return
}
setBnLoading(true)
setBnError('')
setBnResult(null)
try {
const res = await api.postDataManagementFetchBinance({
account_id: aid,
symbols: syms,
data_type: bnDataType,
days: bnDays,
})
setBnResult(res)
} catch (e) {
setBnError(e?.message || '拉取失败')
} finally {
setBnLoading(false)
}
}
const exportDb = () => {
if (!dbResult?.trades) return
const filename = `db_trades_${dbAccountId}_${dbDate || 'today'}_${dbTimeFilter}.json`
downloadJson(dbResult, filename)
}
const exportBinance = () => {
const items = bnResult?.data || bnResult?.items
if (!items) return
const filename = `binance_${bnResult.data_type}_${bnAccountId}.json`
downloadJson({ total: bnResult.total, data_type: bnResult.data_type, data: items }, filename)
}
const today = new Date().toISOString().slice(0, 10)
return (
<div className="data-management">
<h2>数据管理</h2>
<p className="dm-subtitle">查询 DB 交易记录从币安拉取订单/成交支持导出 JSON 做策略分析与对账</p>
{/* 1. 查询 DB 交易 */}
<section className="dm-section">
<h3>查询 DB 交易</h3>
<div className="dm-controls">
<label>
账号
<select value={dbAccountId} onChange={(e) => setDbAccountId(e.target.value)}>
<option value="">请选择</option>
{accounts.map((a) => (
<option key={a.id} value={a.id}>{a.name || `账号${a.id}`}</option>
))}
</select>
</label>
<label>
日期 (YYYY-MM-DD)
<input type="date" value={dbDate} onChange={(e) => setDbDate(e.target.value)} max={today} placeholder="留空=今天" />
</label>
<label>
时间筛选
<select value={dbTimeFilter} onChange={(e) => setDbTimeFilter(e.target.value)}>
<option value="created">按创建时间</option>
<option value="entry">按开仓时间</option>
<option value="exit">按平仓时间</option>
</select>
</label>
<label className="dm-check">
<input type="checkbox" checked={dbReconciledOnly} onChange={(e) => setDbReconciledOnly(e.target.checked)} />
仅可对账 entry/exit orderId
</label>
<label>
交易对
<input type="text" value={dbSymbol} onChange={(e) => setDbSymbol(e.target.value)} placeholder="留空=全部" />
</label>
<button className="btn btn-primary" onClick={queryDbTrades} disabled={dbLoading}>
{dbLoading ? '查询中...' : '查询'}
</button>
</div>
{dbError && <div className="dm-error">{dbError}</div>}
{dbResult && (
<div className="dm-result">
<div className="dm-result-meta">
{dbResult.total}
<button className="btn btn-sm" onClick={exportDb}>导出 JSON</button>
</div>
<div className="dm-table-wrap">
<table className="dm-table">
<thead>
<tr>
<th>ID</th>
<th>交易对</th>
<th>状态</th>
<th>入场价</th>
<th>出场价</th>
<th>盈亏</th>
<th>开仓时间</th>
<th>平仓时间</th>
</tr>
</thead>
<tbody>
{(dbResult.trades || []).slice(0, 100).map((t) => (
<tr key={t.id}>
<td>{t.id}</td>
<td>{t.symbol}</td>
<td>{t.status}</td>
<td>{t.entry_price}</td>
<td>{t.exit_price}</td>
<td>{t.realized_pnl}</td>
<td>{t.entry_time || '-'}</td>
<td>{t.exit_time || '-'}</td>
</tr>
))}
</tbody>
</table>
{(dbResult.trades?.length || 0) > 100 && (
<div className="dm-more">仅显示前 100 {dbResult.total} 导出可获取全部</div>
)}
</div>
</div>
)}
</section>
{/* 2. 从币安拉取 */}
<section className="dm-section">
<h3>从币安拉取</h3>
<div className="dm-controls">
<label>
账号
<select value={bnAccountId} onChange={(e) => setBnAccountId(e.target.value)}>
<option value="">请选择</option>
{accounts.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 }} />
</label>
<label>
数据类型
<select value={bnDataType} onChange={(e) => setBnDataType(e.target.value)}>
<option value="trades">成交记录</option>
<option value="orders">订单列表</option>
</select>
</label>
<label>
最近天数
<select value={bnDays} onChange={(e) => setBnDays(Number(e.target.value))}>
{[1, 2, 3, 5, 7].map((d) => (
<option key={d} value={d}>{d} </option>
))}
</select>
</label>
<button className="btn btn-primary" onClick={fetchBinance} disabled={bnLoading}>
{bnLoading ? '拉取中...' : '拉取'}
</button>
</div>
{bnError && <div className="dm-error">{bnError}</div>}
{bnResult && (
<div className="dm-result">
<div className="dm-result-meta">
{bnResult.total}
<button className="btn btn-sm" onClick={exportBinance}>导出 JSON</button>
</div>
<div className="dm-table-wrap">
<table className="dm-table">
<thead>
<tr>
<th>交易对</th>
<th>orderId</th>
<th>side</th>
<th>price</th>
<th>qty</th>
<th>realizedPnl</th>
<th>time</th>
</tr>
</thead>
<tbody>
{((bnResult.data || bnResult.items) || []).slice(0, 100).map((r, i) => (
<tr key={r.orderId || r.id || i}>
<td>{r._symbol || r.symbol}</td>
<td>{r.orderId}</td>
<td>{r.side}</td>
<td>{r.price}</td>
<td>{r.qty}</td>
<td>{r.realizedPnl}</td>
<td>{r.time ? new Date(r.time).toLocaleString() : r.updateTime ? new Date(r.updateTime).toLocaleString() : '-'}</td>
</tr>
))}
</tbody>
</table>
{((bnResult.data || bnResult.items)?.length || 0) > 100 && (
<div className="dm-more">仅显示前 100 {bnResult.total} 导出可获取全部</div>
)}
</div>
</div>
)}
</section>
</div>
)
}

View File

@ -929,4 +929,37 @@ export const api = {
}
return response.json();
},
// 数据管理(管理员专用)
getDataManagementAccounts: async () => {
const response = await fetch(buildUrl('/api/admin/data/accounts'), { headers: withAuthHeaders() });
if (!response.ok) {
const error = await response.json().catch(() => ({ detail: '获取账号列表失败' }));
throw new Error(error.detail || '获取账号列表失败');
}
return response.json();
},
getDataManagementDbTrades: async (params) => {
const query = new URLSearchParams(params).toString();
const url = query ? `${buildUrl('/api/admin/data/trades')}?${query}` : buildUrl('/api/admin/data/trades');
const response = await fetch(url, { headers: withAuthHeaders() });
if (!response.ok) {
const error = await response.json().catch(() => ({ detail: '查询 DB 交易失败' }));
throw new Error(error.detail || '查询 DB 交易失败');
}
return response.json();
},
postDataManagementFetchBinance: async (params) => {
const query = new URLSearchParams(params).toString();
const url = query ? `${buildUrl('/api/admin/data/binance-fetch')}?${query}` : buildUrl('/api/admin/data/binance-fetch');
const response = await fetch(url, {
method: 'POST',
headers: withAuthHeaders({ 'Content-Type': 'application/json' }),
});
if (!response.ok) {
const error = await response.json().catch(() => ({ detail: '从币安拉取失败' }));
throw new Error(error.detail || '从币安拉取失败');
}
return response.json();
},
};

View File

@ -0,0 +1,147 @@
#!/usr/bin/env python3
"""
从币安拉取最近订单/成交记录供策略分析或与 DB 对照
DB 记录缺失时可用此脚本直接查币安数据
用法:
python scripts/fetch_binance_orders.py --account 2
python scripts/fetch_binance_orders.py --account 2 --symbol BTCUSDT --days 7
python scripts/fetch_binance_orders.py --account 2 --type orders -o binance_orders.json
python scripts/fetch_binance_orders.py --account 2 --symbols ASTERUSDT,FILUSDT,PENGUUSDT
"""
import argparse
import asyncio
import json
import os
import sys
from pathlib import Path
from datetime import datetime, timezone, timedelta
proj = Path(__file__).resolve().parent.parent
if (proj / "backend").exists():
sys.path.insert(0, str(proj / "backend"))
sys.path.insert(0, str(proj))
BEIJING_TZ = timezone(timedelta(hours=8))
def _serialize(obj):
if hasattr(obj, "isoformat"):
return obj.isoformat()
if isinstance(obj, datetime):
return obj.isoformat()
return obj
async def main():
parser = argparse.ArgumentParser(description="从币安拉取订单/成交记录")
parser.add_argument("--account", "-a", type=int, default=None,
help="账号 ID默认 ATS_ACCOUNT_ID 或 1")
parser.add_argument("--symbol", "-s", type=str, default=None,
help="交易对,如 BTCUSDT单 symbol")
parser.add_argument("--symbols", type=str, default=None,
help="多个交易对,逗号分隔,如 ASTERUSDT,FILUSDT")
parser.add_argument("--days", "-d", type=int, default=7,
help="拉取最近 N 天,默认 7币安单次最多 7 天)")
parser.add_argument("--type", "-t", choices=["orders", "trades"], default="trades",
help="orders=订单列表, trades=成交记录(策略分析推荐 trades")
parser.add_argument("-o", "--output", type=str, help="导出到 JSON 文件")
args = parser.parse_args()
account_id = args.account or int(os.getenv("ATS_ACCOUNT_ID") or os.getenv("ACCOUNT_ID") or "1")
symbols = []
if args.symbol:
symbols = [s.strip().upper() for s in args.symbol.split(",") if s.strip()]
elif args.symbols:
symbols = [s.strip().upper() for s in args.symbols.split(",") if s.strip()]
if not symbols:
print("请指定 --symbol 或 --symbols逗号分隔")
print(" 示例: --symbol BTCUSDT 或 --symbols ASTERUSDT,FILUSDT,PENGUUSDT")
sys.exit(1)
try:
from database.models import Account
from trading_system.binance_client import BinanceClient
except ImportError as e:
print(f"导入失败: {e}")
print("请确保在项目根目录运行,且 backend 可访问")
sys.exit(1)
api_key, api_secret, use_testnet, _ = Account.get_credentials(account_id)
if not api_key or not api_secret:
print(f"账号 {account_id} 未配置 API 密钥")
sys.exit(1)
client = BinanceClient(api_key=api_key, api_secret=api_secret, testnet=use_testnet)
try:
await client.connect()
except Exception as e:
print(f"连接币安失败: {e}")
sys.exit(1)
end_ms = int(datetime.now(BEIJING_TZ).timestamp() * 1000)
start_ms = end_ms - args.days * 24 * 3600 * 1000
all_data = []
for sym in symbols:
try:
if args.type == "trades":
rows = await client.client.futures_account_trades(
symbol=sym,
startTime=start_ms,
endTime=end_ms,
limit=1000,
recvWindow=20000,
)
for r in rows:
r["_symbol"] = sym
all_data.extend(rows)
print(f" {sym}: 成交 {len(rows)}")
else:
rows = await client.client.futures_get_all_orders(
symbol=sym,
startTime=start_ms,
endTime=end_ms,
limit=1000,
recvWindow=20000,
)
for r in rows:
r["_symbol"] = sym
all_data.extend(rows)
print(f" {sym}: 订单 {len(rows)}")
except Exception as e:
print(f" {sym}: 失败 {e}")
await asyncio.sleep(0.2)
# 按时间排序
if all_data:
time_key = "time" if "time" in all_data[0] else "updateTime"
all_data.sort(key=lambda x: x.get(time_key, 0), reverse=True)
# 转换大数/日期
out = []
for r in all_data:
row = dict(r)
for k, v in list(row.items()):
if isinstance(v, (datetime,)):
row[k] = v.isoformat()
out.append(row)
print(f"\n账号 {account_id} | 类型 {args.type} | 共 {len(out)}")
if args.output:
with open(args.output, "w", encoding="utf-8") as f:
json.dump(out, f, ensure_ascii=False, indent=2, default=_serialize)
print(f"已导出到 {args.output}")
else:
print(json.dumps(out[:50], ensure_ascii=False, indent=2, default=_serialize))
if len(out) > 50:
print(f"... 仅显示前 50 条,共 {len(out)} 条。可用 -o 导出全部")
if client.client:
await client.client.close_connection()
if __name__ == "__main__":
asyncio.run(main())