feat(data_management): 增强交易数据统计与推算功能
在后端 API 中新增按小时和星期的交易统计功能,优化 `_compute_binance_stats` 函数以支持更细致的统计分析。同时,新增 `_enrich_trades_with_derived` 函数,补充交易记录的推算字段,包括入场价、交易小时和星期,提升策略分析的便利性。前端 `DataManagement` 组件更新,展示按小时和星期的统计信息,增强用户对交易数据的可视化理解。
This commit is contained in:
parent
1e478c8428
commit
3b0526f392
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -301,6 +301,26 @@ export default function DataManagement() {
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
{bnStats.by_weekday && Object.keys(bnStats.by_weekday).length > 0 && (
|
||||
<div className="dm-stat-item dm-stat-wide">
|
||||
<span className="dm-stat-label">按星期</span>
|
||||
<div className="dm-stat-symbols">
|
||||
{Object.entries(bnStats.by_weekday).map(([day, v]) => (
|
||||
<span key={day} className="dm-stat-sym">{day}: {v.count}笔, 盈亏{v.pnl}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{bnStats.by_hour && Object.keys(bnStats.by_hour).length > 0 && (
|
||||
<div className="dm-stat-item dm-stat-wide">
|
||||
<span className="dm-stat-label">按小时(北京)</span>
|
||||
<div className="dm-stat-symbols dm-stat-hours">
|
||||
{Object.entries(bnStats.by_hour).map(([h, v]) => (
|
||||
<span key={h} className="dm-stat-sym">{h}时: {v.count}笔, 盈亏{v.pnl}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{!isTrades && (bnStats.by_status || bnStats.filled_count !== undefined) && (
|
||||
|
|
@ -349,11 +369,13 @@ export default function DataManagement() {
|
|||
{isTrades ? (
|
||||
<>
|
||||
<th>positionSide</th>
|
||||
<th>price</th>
|
||||
<th>出场价</th>
|
||||
<th>推算入场价</th>
|
||||
<th>qty</th>
|
||||
<th>quoteQty</th>
|
||||
<th>realizedPnl</th>
|
||||
<th>commission</th>
|
||||
<th>小时</th>
|
||||
<th>buyer</th>
|
||||
<th>maker</th>
|
||||
</>
|
||||
|
|
@ -383,10 +405,12 @@ export default function DataManagement() {
|
|||
<>
|
||||
<td>{r.positionSide || '-'}</td>
|
||||
<td>{r.price}</td>
|
||||
<td>{r._approx_entry_price != null ? r._approx_entry_price : '-'}</td>
|
||||
<td>{r.qty}</td>
|
||||
<td>{r.quoteQty}</td>
|
||||
<td className={(r.realizedPnl || 0) >= 0 ? 'dm-profit' : 'dm-loss'}>{r.realizedPnl}</td>
|
||||
<td>{r.commission}</td>
|
||||
<td>{r._trade_hour != null ? `${r._trade_hour}时` : '-'}</td>
|
||||
<td>{r.buyer ? '买' : '卖'}</td>
|
||||
<td>{r.maker ? '是' : '否'}</td>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -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。
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user