diff --git a/backend/api/main.py b/backend/api/main.py
index 13e8e04..83ced05 100644
--- a/backend/api/main.py
+++ b/backend/api/main.py
@@ -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)
diff --git a/backend/api/routes/data_management.py b/backend/api/routes/data_management.py
new file mode 100644
index 0000000..1a2679c
--- /dev/null
+++ b/backend/api/routes/data_management.py
@@ -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()
diff --git a/docs/DB与币安订单对账说明.md b/docs/DB与币安订单对账说明.md
index 54de34b..7ee8a65 100644
--- a/docs/DB与币安订单对账说明.md
+++ b/docs/DB与币安订单对账说明.md
@@ -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`
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index 42809da..33fc8d9 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -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 && (
<>
全局配置
+ 数据管理
日志监控
>
)}
@@ -115,6 +117,7 @@ function App() {
} />
} />
:
无权限
} />
+ : 无权限
} />
: 无权限
} />
diff --git a/frontend/src/components/DataManagement.css b/frontend/src/components/DataManagement.css
new file mode 100644
index 0000000..89973ab
--- /dev/null
+++ b/frontend/src/components/DataManagement.css
@@ -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;
+}
diff --git a/frontend/src/components/DataManagement.jsx b/frontend/src/components/DataManagement.jsx
new file mode 100644
index 0000000..d4da0b6
--- /dev/null
+++ b/frontend/src/components/DataManagement.jsx
@@ -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 (
+
+
数据管理
+
查询 DB 交易记录、从币安拉取订单/成交,支持导出 JSON 做策略分析与对账。
+
+ {/* 1. 查询 DB 交易 */}
+
+ 查询 DB 交易
+
+
+
+
+
+
+
+
+ {dbError && {dbError}
}
+ {dbResult && (
+
+
+ 共 {dbResult.total} 条
+
+
+
+
+
+
+ | ID |
+ 交易对 |
+ 状态 |
+ 入场价 |
+ 出场价 |
+ 盈亏 |
+ 开仓时间 |
+ 平仓时间 |
+
+
+
+ {(dbResult.trades || []).slice(0, 100).map((t) => (
+
+ | {t.id} |
+ {t.symbol} |
+ {t.status} |
+ {t.entry_price} |
+ {t.exit_price} |
+ {t.realized_pnl} |
+ {t.entry_time || '-'} |
+ {t.exit_time || '-'} |
+
+ ))}
+
+
+ {(dbResult.trades?.length || 0) > 100 && (
+
仅显示前 100 条,共 {dbResult.total} 条。导出可获取全部。
+ )}
+
+
+ )}
+
+
+ {/* 2. 从币安拉取 */}
+
+ 从币安拉取
+
+
+
+
+
+
+
+ {bnError && {bnError}
}
+ {bnResult && (
+
+
+ 共 {bnResult.total} 条
+
+
+
+
+
+
+ | 交易对 |
+ orderId |
+ side |
+ price |
+ qty |
+ realizedPnl |
+ time |
+
+
+
+ {((bnResult.data || bnResult.items) || []).slice(0, 100).map((r, i) => (
+
+ | {r._symbol || r.symbol} |
+ {r.orderId} |
+ {r.side} |
+ {r.price} |
+ {r.qty} |
+ {r.realizedPnl} |
+ {r.time ? new Date(r.time).toLocaleString() : r.updateTime ? new Date(r.updateTime).toLocaleString() : '-'} |
+
+ ))}
+
+
+ {((bnResult.data || bnResult.items)?.length || 0) > 100 && (
+
仅显示前 100 条,共 {bnResult.total} 条。导出可获取全部。
+ )}
+
+
+ )}
+
+
+ )
+}
diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js
index 5b5195e..6287769 100644
--- a/frontend/src/services/api.js
+++ b/frontend/src/services/api.js
@@ -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();
+ },
};
diff --git a/scripts/fetch_binance_orders.py b/scripts/fetch_binance_orders.py
new file mode 100644
index 0000000..dbb9814
--- /dev/null
+++ b/scripts/fetch_binance_orders.py
@@ -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())