diff --git a/backend/api/routes/stats.py b/backend/api/routes/stats.py index f516447..352517f 100644 --- a/backend/api/routes/stats.py +++ b/backend/api/routes/stats.py @@ -30,7 +30,8 @@ async def get_admin_dashboard_stats(user: Dict[str, Any] = Depends(get_admin_use active_accounts = 0 for acc in accounts: aid = acc["id"] - snapshots = AccountSnapshot.get_recent(1, account_id=aid) + # 取最近 30 天内的快照,再取最新一条,避免“仅 1 天”导致无数据 + snapshots = AccountSnapshot.get_recent(30, account_id=aid) acc_stat = { "id": aid, "name": acc["name"], diff --git a/backend/api/routes/trades.py b/backend/api/routes/trades.py index 9c74428..79ea937 100644 --- a/backend/api/routes/trades.py +++ b/backend/api/routes/trades.py @@ -18,6 +18,7 @@ sys.path.insert(0, str(project_root)) sys.path.insert(0, str(project_root / 'backend')) from database.models import Trade, Account +from database.connection import db from api.auth_deps import get_account_id router = APIRouter() @@ -83,6 +84,7 @@ async def get_trades( include_sync: bool = Query(True, description="是否包含 entry_reason 为 sync_recovered 的补建/同步单(默认包含,便于订单记录与币安一致)"), reconciled_only: bool = Query(True, description="仅返回可对账记录(有 entry_order_id,已平仓的还有 exit_order_id),与币安一致,统计真实"), limit: int = Query(100, ge=1, le=1000, description="返回记录数限制"), + source: Optional[str] = Query(None, description="数据源: 'binance' 从 binance_trades 查(与交易所一致,需先运行 sync_binance_orders.py 同步)"), ): """ 获取交易记录 @@ -126,6 +128,59 @@ async def get_trades( except ValueError: logger.warning(f"无效的结束日期格式: {end_date}") + # 数据源:币安成交(binance_trades),与管理员数据管理一致,更可靠 + if (source or "").strip().lower() == "binance": + beijing_tz = timezone(timedelta(hours=8)) + now = datetime.now(beijing_tz) + end_ts = end_timestamp or int(now.timestamp()) + start_ts = start_timestamp or (end_ts - 7 * 24 * 3600) + start_ms = start_ts * 1000 + end_ms = end_ts * 1000 + try: + q = """SELECT * FROM binance_trades + WHERE account_id = %s AND trade_time >= %s AND trade_time <= %s""" + params = [account_id, start_ms, end_ms] + if symbol: + q += " AND symbol = %s" + params.append(symbol.strip().upper()) + q += " ORDER BY trade_time DESC LIMIT %s" + params.append(min(limit, 1000)) + rows = db.execute_query(q, params) + except Exception as e: + logger.warning(f"查询 binance_trades 失败(请确认已执行 add_binance_sync_tables.sql 并运行过 sync_binance_orders.py): {e}") + rows = [] + out = [] + for r in (rows or []): + row = dict(r) + out.append({ + "source": "binance", + "trade_time": row.get("trade_time"), + "symbol": row.get("symbol") or "", + "side": row.get("side") or "", + "price": float(row.get("price") or 0), + "qty": float(row.get("qty") or 0), + "quote_qty": float(row.get("quote_qty") or 0), + "realized_pnl": float(row.get("realized_pnl") or 0), + "commission": float(row.get("commission") or 0), + "commission_asset": row.get("commission_asset") or "", + "order_id": row.get("order_id"), + "trade_id": row.get("trade_id"), + "maker": bool(row.get("maker")), + }) + return { + "total": len(out), + "trades": out, + "source": "binance", + "filters": { + "start_timestamp": start_ts, + "end_timestamp": end_ts, + "start_date": datetime.fromtimestamp(start_ts).strftime('%Y-%m-%d %H:%M:%S'), + "end_date": datetime.fromtimestamp(end_ts).strftime('%Y-%m-%d %H:%M:%S'), + "period": period, + "symbol": symbol, + }, + } + trades = Trade.get_all( start_timestamp, end_timestamp, symbol, status, trade_type, exit_reason, account_id=account_id, time_filter=time_filter or "exit", diff --git a/docs/trades_vs_binance_trades.md b/docs/trades_vs_binance_trades.md new file mode 100644 index 0000000..96cf7cf --- /dev/null +++ b/docs/trades_vs_binance_trades.md @@ -0,0 +1,37 @@ +# trades 表 vs binance_trades 表:角色与使用建议 + +## 两张表的角色 + +### trades 表(本地交易记录) + +- **数据来源**:本系统在**开仓/平仓、补建、同步**时写入或更新。 +- **粒度**:一笔「回合」一条记录(开仓 → 平仓),含 entry_price、exit_price、entry_time、exit_time、pnl、exit_reason、entry_order_id、exit_order_id 等。 +- **主要用途**:**主要支持交易与监控相关**——实盘运行时的持仓状态、风控、冷却、对账、pending 补全等;用户端「交易记录」可选「本地记录」查看本系统回合,但**查询/分析订单建议以 binance_trades 为准**。 + +### binance_trades 表(币安成交同步) + +- **数据来源**:定时任务 `scripts/sync_binance_orders.py` 从币安 API 拉取 **userTrades(成交)** 入库。 +- **同步节奏与延时**:脚本按 **每 3 小时的第 0 分钟** 执行(crontab 示例:`0 */3 * * *`),存在一定延时性,页面看到的「币安成交」最多可能滞后约 3 小时;如需最新可手动执行一次脚本。 +- **粒度**:**一笔成交一条记录**(含 trade_id、order_id、symbol、side、price、qty、realized_pnl、commission、trade_time 等)。 +- **主要用途**: + 1. **管理员「数据管理」**:按账号/时间/交易对查询已同步的币安成交,做分析与导出。 + 2. **统计与分析**:`TradeStats` 聚合(7 天统计、按交易对、按小时等)**优先使用 binance_trades**,无数据时再回退到 trades。 + +## 结论与建议 + +1. **分析、统计、查询「订单/成交」** + **以 binance_trades 为准即可**。数据与交易所一致,更适合做盈亏分析、策略回看。系统已按「优先 binance_trades,无则 trades」实现。 + +2. **用户端「交易记录」为何经常没数据?** + 因为当前只查 **trades**。若实盘不全在本系统跑、或同步/补建不完整,trades 会少或为空。 + **建议**:用户端「交易记录」**默认从 binance_trades 查**(按当前账号 + 时间范围),与管理员在数据管理里看到的来源一致,更可靠。 + +3. **trades 表是否还有用?** + **有用,但角色已明确为「主要支持交易、监控」**:开平仓状态、风控、冷却、对账、pending 补全等仍依赖 trades;**订单查询与盈亏分析**以 binance_trades 为主。 + +## 实施情况 + +- 用户端「交易记录」支持数据源切换: + - **币安成交(binance_trades)**:默认。按当前账号从 DB 已同步的 binance_trades 查询,展示成交明细(时间、方向、价格、数量、已实现盈亏、手续费等)。需先在服务器运行 `scripts/sync_binance_orders.py` 同步。**数据由定时任务每 3 小时的第 0 分钟同步,存在一定延时(最多约 3 小时)。** + - **本地记录(trades)**:保留原有逻辑,展示回合级记录(开/平仓时间、入场/出场价、平仓原因等)。 +- 后端:`GET /api/trades?source=binance&period=7d&symbol=...` 使用当前账号从 binance_trades 查询并返回列表,与管理员数据管理同源。 diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 2203c31..8caff43 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -84,7 +84,6 @@ function App() { )} {isAdmin && ( <> - 仪表板
管理中心
diff --git a/frontend/src/components/DataManagement.jsx b/frontend/src/components/DataManagement.jsx index edf73cd..d1cec4d 100644 --- a/frontend/src/components/DataManagement.jsx +++ b/frontend/src/components/DataManagement.jsx @@ -201,7 +201,7 @@ export default function DataManagement() { )} - {/* 2. 币安订单/成交(从 DB 查询,由定时任务 scripts/sync_binance_orders.py 同步) */} + {/* 2. 币安订单/成交(从 DB 查询,由定时任务 scripts/sync_binance_orders.py 每 3 小时同步,存在一定延时) */}

币安订单/成交查询

@@ -243,7 +243,7 @@ export default function DataManagement() {
共 {bnResult.total} 条 · 查询 {bnResult.symbols_queried ?? '-'} 个交易对 - {bnResult.source === 'db' && 来自 DB(定时同步)} + {bnResult.source === 'db' && 来自 DB(定时同步)}
diff --git a/frontend/src/components/TradeList.jsx b/frontend/src/components/TradeList.jsx index 590f6df..ba34afd 100644 --- a/frontend/src/components/TradeList.jsx +++ b/frontend/src/components/TradeList.jsx @@ -26,16 +26,17 @@ const TradeList = () => { const [syncResult, setSyncResult] = useState(null) // 同步结果 const [syncDays, setSyncDays] = useState(7) // 同步天数 const [syncAllSymbols, setSyncAllSymbols] = useState(false) // 是否同步所有交易对的订单 + const [dataSource, setDataSource] = useState('binance') // 'binance' | 'local',默认币安成交更可靠 useEffect(() => { loadData() - }, [accountId, reconciledOnly, timeFilter, period, useCustomDate, startDate, endDate, symbol, status, tradeType, exitReason]) // 筛选条件变化时重新加载(含按创建时间、快速时间段) + }, [accountId, dataSource, reconciledOnly, timeFilter, period, useCustomDate, startDate, endDate, symbol, status, tradeType, exitReason]) const loadData = async () => { setLoading(true) try { const params = { - limit: 100 + limit: 500 } // 如果使用快速时间段筛选 @@ -48,18 +49,36 @@ const TradeList = () => { } if (symbol) params.symbol = symbol - if (status) params.status = status - if (tradeType) params.trade_type = tradeType - if (exitReason) params.exit_reason = exitReason - params.reconciled_only = reconciledOnly - params.time_filter = timeFilter || 'exit' + if (dataSource === 'binance') { + params.source = 'binance' + } else { + if (status) params.status = status + if (tradeType) params.trade_type = tradeType + if (exitReason) params.exit_reason = exitReason + params.reconciled_only = reconciledOnly + params.time_filter = timeFilter || 'exit' + } - const [tradesData, statsData] = await Promise.all([ - api.getTrades(params), - api.getTradeStats(params) - ]) - setTrades(tradesData.trades || []) - setStats(statsData) + const tradesPromise = api.getTrades(params) + const statsPromise = dataSource === 'local' ? api.getTradeStats({ ...params }) : Promise.resolve(null) + + const [tradesRes, statsData] = await Promise.all([tradesPromise, statsPromise]) + const tradesList = tradesRes.trades || [] + setTrades(tradesList) + + if (dataSource === 'binance') { + const totalPnl = tradesList.reduce((s, t) => s + (Number(t.realized_pnl) || 0), 0) + const totalCommission = tradesList.reduce((s, t) => s + (Number(t.commission) || 0), 0) + setStats({ + total_trades: tradesList.length, + total_pnl: totalPnl - totalCommission, + total_realized_pnl: totalPnl, + total_commission: totalCommission, + source: 'binance', + }) + } else { + setStats(statsData) + } } catch (error) { console.error('Failed to load trades:', error) } finally { @@ -141,7 +160,21 @@ const TradeList = () => { return } - const exportData = trades.map(trade => { + const isBinance = trades[0] && trades[0].source === 'binance' + const exportData = isBinance + ? trades.map(t => ({ + 成交时间: t.trade_time ? new Date(t.trade_time).toISOString() : '', + 交易对: t.symbol, + 方向: t.side, + 价格: t.price, + 数量: t.qty, + 已实现盈亏: t.realized_pnl, + 手续费: t.commission, + 手续费资产: t.commission_asset || 'USDT', + 订单号: t.order_id, + 成交ID: t.trade_id, + })) + : trades.map(trade => { const notional = trade.notional_usdt !== undefined && trade.notional_usdt !== null ? parseFloat(trade.notional_usdt) : (trade.entry_value_usdt !== undefined && trade.entry_value_usdt !== null @@ -361,7 +394,9 @@ const TradeList = () => {

交易记录

- 说明:每条记录代表一笔完整的交易(开仓+平仓),统计总盈亏时每条记录只计算一次。默认「仅可对账」:只显示有开仓/平仓订单号的记录,统计与币安一致。时间依据:按平仓时间=今日平仓+今日开仓未平仓;按开仓时间=实际入场时间,适合策略分析;按创建时间=记录写入 DB 时间。 + {dataSource === 'binance' + ? '数据源:币安成交(binance_trades 表,与交易所一致)。由定时任务每 3 小时的第 0 分钟同步,存在一定延时(最多约 3 小时);需先在服务器运行 scripts/sync_binance_orders.py。' + : '说明:每条记录代表一笔完整的交易(开仓+平仓),统计总盈亏时每条记录只计算一次。默认「仅可对账」:只显示有开仓/平仓订单号的记录,统计与币安一致。时间依据:按平仓时间=今日平仓+今日开仓未平仓;按开仓时间=实际入场时间,适合策略分析;按创建时间=记录写入 DB 时间。'}

{ {/* 筛选面板 */}
+
+ +
+ + +
+
@@ -650,7 +704,7 @@ const TradeList = () => {
{ - stats && ( + stats && stats.source !== 'binance' && (
{/* 卡片式统计(始终显示) */}
@@ -818,7 +872,6 @@ const TradeList = () => {
- )}
) } @@ -827,12 +880,61 @@ const TradeList = () => { trades.length === 0 ? (
暂无交易记录
- {reconciledOnly && ( + {dataSource === 'binance' && ( +

+ 暂无已同步的币安成交记录。数据由定时任务每 3 小时同步(存在一定延时)。请先在服务器运行 scripts/sync_binance_orders.py 或稍后刷新,也可切换为「本地记录」查看本系统 trades 表。 +

+ )} + {dataSource === 'local' && reconciledOnly && (

若币安今日有订单但此处为空,可先点击右上角「同步订单」补全开仓/平仓订单号,或取消勾选「仅可对账」查看全部记录。

)}
+ ) : (trades[0] && trades[0].source === 'binance') ? ( + <> +
+ + + + + + + + + + + + + + + {trades.map((t, i) => { + const ts = t.trade_time + const timeStr = ts ? new Date(ts).toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }) : '-' + const pnl = Number(t.realized_pnl) || 0 + const comm = Number(t.commission) || 0 + return ( + + + + + + + + + + + ) + })} + +
成交时间交易对方向价格数量已实现盈亏手续费订单号
{timeStr}{t.symbol}{(t.side || '').toUpperCase() === 'BUY' ? '买' : '卖'}{Number(t.price).toFixed(4)}{Number(t.qty).toFixed(4)}= 0 ? 'positive' : 'negative'}>{pnl.toFixed(2)}{comm.toFixed(4)} {t.commission_asset || 'USDT'}{t.order_id ?? '-'}
+
+ {stats && stats.source === 'binance' && ( +
+ 共 {stats.total_trades} 笔,已实现盈亏 {Number(stats.total_realized_pnl || 0).toFixed(2)} USDT,手续费 {Number(stats.total_commission || 0).toFixed(2)} USDT,净盈亏 {Number(stats.total_pnl || 0).toFixed(2)} USDT +
+ )} + ) : ( <> {/* 桌面端表格:用横向滚动包裹,避免整页过宽 */} diff --git a/scripts/SYNC_BINANCE_README.md b/scripts/SYNC_BINANCE_README.md index 52aeded..1a91067 100644 --- a/scripts/SYNC_BINANCE_README.md +++ b/scripts/SYNC_BINANCE_README.md @@ -28,12 +28,14 @@ python scripts/sync_binance_orders.py --delay-between-accounts 60 ## 3. Crontab 配置示例 -每 3 小时执行一次(与 6 小时拉取窗口重叠,便于去重): +每 3 小时的第 0 分钟执行一次(与 6 小时拉取窗口重叠,便于去重): ```cron 0 */3 * * * cd /path/to/auto_trade_sys && /path/to/.venv/bin/python scripts/sync_binance_orders.py >> logs/sync_binance.log 2>&1 ``` +**延时说明**:因定时任务为每 3 小时跑一次,页面或统计里看到的「币安成交」数据存在一定延时,最多可能滞后约 3 小时;如需最新可手动执行一次脚本。 + 或每 6 小时: ```cron diff --git a/scripts/sync_binance_orders.py b/scripts/sync_binance_orders.py index c9415da..af6b7a1 100644 --- a/scripts/sync_binance_orders.py +++ b/scripts/sync_binance_orders.py @@ -1,8 +1,7 @@ #!/usr/bin/env python3 """ 定时任务:从币安拉取各账号最近 6 小时的订单/成交数据,去重写入 DB。 -供 crontab 定时执行,如: 0 */3 * * * cd /path/to/project && python scripts/sync_binance_orders.py - +供 crontab 定时执行,建议每 3 小时的第 0 分钟执行(如 0 */3 * * *),存在一定延时性。 用法: python scripts/sync_binance_orders.py # 所有有效账号,最近 6 小时 python scripts/sync_binance_orders.py -a 2 # 指定账号