diff --git a/backend/api/routes/data_management.py b/backend/api/routes/data_management.py
index 94b7307..2cd4b38 100644
--- a/backend/api/routes/data_management.py
+++ b/backend/api/routes/data_management.py
@@ -116,6 +116,24 @@ def _compute_binance_stats(data: list, data_type: str) -> dict:
}
for k, v in sorted(by_symbol.items())
}
+
+ by_hour = {}
+ by_weekday = {}
+ weekday_names = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"]
+ for r in valid:
+ t = r.get("time") or r.get("trade_time") or 0
+ if t:
+ dt = datetime.fromtimestamp(t / 1000, tz=BEIJING_TZ)
+ h = dt.hour
+ wd = dt.weekday()
+ by_hour[h] = by_hour.get(h, {"count": 0, "pnl": 0.0})
+ by_hour[h]["count"] += 1
+ by_hour[h]["pnl"] += float(r.get("realizedPnl") or 0)
+ by_weekday[wd] = by_weekday.get(wd, {"count": 0, "pnl": 0.0})
+ by_weekday[wd]["count"] += 1
+ by_weekday[wd]["pnl"] += float(r.get("realizedPnl") or 0)
+ stats["by_hour"] = {str(k): {"count": v["count"], "pnl": round(v["pnl"], 4)} for k, v in sorted(by_hour.items())}
+ stats["by_weekday"] = {weekday_names[k]: {"count": v["count"], "pnl": round(v["pnl"], 4)} for k, v in sorted(by_weekday.items())}
else:
by_status = {}
by_type = {}
@@ -223,6 +241,32 @@ async def query_db_trades(
return {"total": len(out), "trades": out}
+def _enrich_trades_with_derived(trades: list) -> list:
+ """补充推算字段:入场价、交易小时、星期,便于策略分析"""
+ result = []
+ for r in trades:
+ out = dict(r)
+ t = r.get("time") or 0
+ if t:
+ dt = datetime.fromtimestamp(t / 1000, tz=BEIJING_TZ)
+ out["_trade_hour"] = dt.hour
+ out["_trade_weekday"] = dt.weekday()
+ out["_trade_date"] = dt.strftime("%Y-%m-%d")
+ pnl = float(r.get("realizedPnl") or 0)
+ qty = float(r.get("qty") or 0)
+ price = float(r.get("price") or 0)
+ side = (r.get("side") or "").upper()
+ if qty and pnl != 0 and side:
+ if side == "SELL":
+ out["_approx_entry_price"] = round(price - pnl / qty, 8)
+ else:
+ out["_approx_entry_price"] = round(price + pnl / qty, 8)
+ else:
+ out["_approx_entry_price"] = None
+ result.append(out)
+ return result
+
+
def _binance_row_to_api_format(row: dict, data_type: str) -> dict:
"""将 DB 行转换为前端/导出期望的币安 API 格式"""
if data_type == "trades":
@@ -315,6 +359,8 @@ async def query_binance_data_from_db(
raise HTTPException(status_code=500, detail=f"查询失败(请确认已执行 add_binance_sync_tables.sql 并运行过同步脚本): {e}")
all_data = [_binance_row_to_api_format(dict(r), data_type) for r in (rows or [])]
+ if data_type == "trades":
+ all_data = _enrich_trades_with_derived(all_data)
symbols_queried = len(symbol_list) if symbol_list else len({(r or {}).get("symbol") for r in (rows or []) if (r or {}).get("symbol")})
stats = _compute_binance_stats(all_data, data_type)
diff --git a/frontend/src/components/DataManagement.css b/frontend/src/components/DataManagement.css
index a2bf9d2..8f8b241 100644
--- a/frontend/src/components/DataManagement.css
+++ b/frontend/src/components/DataManagement.css
@@ -105,6 +105,7 @@
.dm-stat-profit { color: #059669; font-weight: 600; }
.dm-stat-loss { color: #dc2626; font-weight: 600; }
.dm-stat-symbols { display: flex; flex-wrap: wrap; gap: 8px 16px; font-size: 12px; }
+.dm-stat-hours { max-height: 120px; overflow-y: auto; }
.dm-stat-sym { background: #fff; padding: 2px 8px; border-radius: 4px; }
.dm-table .dm-profit { color: #059669; }
.dm-table .dm-loss { color: #dc2626; }
diff --git a/frontend/src/components/DataManagement.jsx b/frontend/src/components/DataManagement.jsx
index dfe9f75..edf73cd 100644
--- a/frontend/src/components/DataManagement.jsx
+++ b/frontend/src/components/DataManagement.jsx
@@ -301,6 +301,26 @@ export default function DataManagement() {
)}
+ {bnStats.by_weekday && Object.keys(bnStats.by_weekday).length > 0 && (
+
+
按星期
+
+ {Object.entries(bnStats.by_weekday).map(([day, v]) => (
+ {day}: {v.count}笔, 盈亏{v.pnl}
+ ))}
+
+
+ )}
+ {bnStats.by_hour && Object.keys(bnStats.by_hour).length > 0 && (
+
+
按小时(北京)
+
+ {Object.entries(bnStats.by_hour).map(([h, v]) => (
+ {h}时: {v.count}笔, 盈亏{v.pnl}
+ ))}
+
+
+ )}
>
)}
{!isTrades && (bnStats.by_status || bnStats.filled_count !== undefined) && (
@@ -349,11 +369,13 @@ export default function DataManagement() {
{isTrades ? (
<>
positionSide |
- price |
+ 出场价 |
+ 推算入场价 |
qty |
quoteQty |
realizedPnl |
commission |
+ 小时 |
buyer |
maker |
>
@@ -383,10 +405,12 @@ export default function DataManagement() {
<>
{r.positionSide || '-'} |
{r.price} |
+ {r._approx_entry_price != null ? r._approx_entry_price : '-'} |
{r.qty} |
{r.quoteQty} |
= 0 ? 'dm-profit' : 'dm-loss'}>{r.realizedPnl} |
{r.commission} |
+ {r._trade_hour != null ? `${r._trade_hour}时` : '-'} |
{r.buyer ? '买' : '卖'} |
{r.maker ? '是' : '否'} |
>
diff --git a/scripts/SYNC_BINANCE_README.md b/scripts/SYNC_BINANCE_README.md
index 0a93612..52aeded 100644
--- a/scripts/SYNC_BINANCE_README.md
+++ b/scripts/SYNC_BINANCE_README.md
@@ -21,6 +21,9 @@ python scripts/sync_binance_orders.py -a 2
# 拉取最近 12 小时
python scripts/sync_binance_orders.py --hours 12
+
+# 多账号时减少账号间隔(默认 90 秒,避免限频)
+python scripts/sync_binance_orders.py --delay-between-accounts 60
```
## 3. Crontab 配置示例
@@ -37,6 +40,12 @@ python scripts/sync_binance_orders.py --hours 12
0 */6 * * * cd /path/to/auto_trade_sys && /path/to/.venv/bin/python scripts/sync_binance_orders.py >> logs/sync_binance.log 2>&1
```
-## 4. 数据管理
+## 4. 限频说明
+
+- 多账号时每个账号之间默认等待 90 秒,可用 `--delay-between-accounts` 调整
+- 单账号内已降低并发(Semaphore 2)和请求间隔,减少触发 "Way too many requests" 封 IP
+- 若已被封,需等待提示时间后重试;建议 crontab 间隔不少于 3 小时
+
+## 5. 数据管理
管理后台「数据管理」-「币安订单/成交查询」从 DB 读取,不再调用币安 API。
diff --git a/scripts/sync_binance_orders.py b/scripts/sync_binance_orders.py
index fa1687c..c9415da 100644
--- a/scripts/sync_binance_orders.py
+++ b/scripts/sync_binance_orders.py
@@ -46,7 +46,7 @@ async def _get_active_symbols(client, start_ms: int, end_ms: int) -> list:
current_end = oldest - 1
if current_end < start_ms:
break
- await asyncio.sleep(0.15)
+ await asyncio.sleep(0.4)
return sorted(symbols)
except Exception:
return []
@@ -79,7 +79,7 @@ async def sync_account(account_id: int, hours: int = 6) -> tuple:
if not sym_list:
return 0, 0, "无法获取交易对列表"
- sem = asyncio.Semaphore(5)
+ sem = asyncio.Semaphore(2)
async def _fetch_trades(sym):
async with sem:
@@ -95,7 +95,7 @@ async def sync_account(account_id: int, hours: int = 6) -> tuple:
except Exception:
return []
finally:
- await asyncio.sleep(0.12)
+ await asyncio.sleep(0.35)
async def _fetch_orders(sym):
async with sem:
@@ -111,7 +111,7 @@ async def sync_account(account_id: int, hours: int = 6) -> tuple:
except Exception:
return []
finally:
- await asyncio.sleep(0.12)
+ await asyncio.sleep(0.35)
trades_chunks = await asyncio.gather(*[_fetch_trades(s) for s in sym_list])
orders_chunks = await asyncio.gather(*[_fetch_orders(s) for s in sym_list])
@@ -209,8 +209,10 @@ def main():
parser = argparse.ArgumentParser(description="同步币安订单/成交到 DB(供 crontab 定时执行)")
parser.add_argument("-a", "--account", type=int, default=None, help="指定账号 ID,不传则同步所有有效账号")
parser.add_argument("--hours", type=int, default=6, help="拉取最近 N 小时,默认 6")
+ parser.add_argument("--delay-between-accounts", type=int, default=90, help="多账号时,每个账号之间等待秒数,默认 90(避免触发币安限频)")
args = parser.parse_args()
hours = args.hours
+ delay_between = max(0, args.delay_between_accounts)
try:
from database.models import Account
@@ -245,7 +247,11 @@ def main():
sys.stdout.flush()
async def run_all():
- for acc in to_sync:
+ for i, acc in enumerate(to_sync):
+ if i > 0 and delay_between > 0:
+ print(f" 等待 {delay_between} 秒后同步下一账号(避免限频)...")
+ sys.stdout.flush()
+ await asyncio.sleep(delay_between)
aid = acc["id"]
name = acc.get("name") or f"账号{aid}"
tr, ord_cnt, err = await sync_account(aid, hours)