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 && ( <> - 仪表板
- 说明:每条记录代表一笔完整的交易(开仓+平仓),统计总盈亏时每条记录只计算一次。默认「仅可对账」:只显示有开仓/平仓订单号的记录,统计与币安一致。时间依据:按平仓时间=今日平仓+今日开仓未平仓;按开仓时间=实际入场时间,适合策略分析;按创建时间=记录写入 DB 时间。 + {dataSource === 'binance' + ? '数据源:币安成交(binance_trades 表,与交易所一致)。由定时任务每 3 小时的第 0 分钟同步,存在一定延时(最多约 3 小时);需先在服务器运行 scripts/sync_binance_orders.py。' + : '说明:每条记录代表一笔完整的交易(开仓+平仓),统计总盈亏时每条记录只计算一次。默认「仅可对账」:只显示有开仓/平仓订单号的记录,统计与币安一致。时间依据:按平仓时间=今日平仓+今日开仓未平仓;按开仓时间=实际入场时间,适合策略分析;按创建时间=记录写入 DB 时间。'}
+ 暂无已同步的币安成交记录。数据由定时任务每 3 小时同步(存在一定延时)。请先在服务器运行 scripts/sync_binance_orders.py 或稍后刷新,也可切换为「本地记录」查看本系统 trades 表。 +
+ )} + {dataSource === 'local' && reconciledOnly && (若币安今日有订单但此处为空,可先点击右上角「同步订单」补全开仓/平仓订单号,或取消勾选「仅可对账」查看全部记录。
)}| 成交时间 | +交易对 | +方向 | +价格 | +数量 | +已实现盈亏 | +手续费 | +订单号 | +
|---|---|---|---|---|---|---|---|
| {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 ?? '-'} | +