diff --git a/backend/api/routes/trades.py b/backend/api/routes/trades.py index 654323d..6e8e924 100644 --- a/backend/api/routes/trades.py +++ b/backend/api/routes/trades.py @@ -4,6 +4,7 @@ from fastapi import APIRouter, Query, HTTPException from typing import Optional from datetime import datetime, timedelta +from collections import Counter import sys from pathlib import Path import logging @@ -226,6 +227,23 @@ async def get_trade_stats( sum(abs(float(t["pnl"])) for t in loss_trades) / len(loss_trades) if loss_trades else 0.0 ) win_loss_ratio = (avg_win_pnl / avg_loss_pnl_abs) if avg_loss_pnl_abs > 0 else None + + # 平仓原因分布(用来快速定位胜率低的主要来源:止损/止盈/同步等) + exit_reason_counts = Counter((t.get("exit_reason") or "unknown") for t in meaningful_trades) + + # 平均持仓时长(分钟):优先使用 duration_minutes 字段(若为空则跳过) + durations = [] + for t in meaningful_trades: + dm = t.get("duration_minutes") + try: + if dm is None: + continue + dm_f = float(dm) + if dm_f >= 0: + durations.append(dm_f) + except Exception: + continue + avg_duration_minutes = (sum(durations) / len(durations)) if durations else None stats = { "total_trades": len(trades), @@ -243,6 +261,8 @@ async def get_trade_stats( "avg_loss_pnl_abs": avg_loss_pnl_abs, "avg_win_loss_ratio": win_loss_ratio, "avg_win_loss_ratio_target": 3.0, + "exit_reason_counts": dict(exit_reason_counts), + "avg_duration_minutes": avg_duration_minutes, # 总交易量(名义下单量口径):优先使用 notional_usdt(新字段),否则回退 entry_price * quantity "total_notional_usdt": sum( float(t.get('notional_usdt') or (float(t.get('entry_price', 0)) * float(t.get('quantity', 0)))) diff --git a/frontend/src/components/TradeList.jsx b/frontend/src/components/TradeList.jsx index ebda22a..8f669f7 100644 --- a/frontend/src/components/TradeList.jsx +++ b/frontend/src/components/TradeList.jsx @@ -266,6 +266,39 @@ const TradeList = () => { {stats.avg_pnl.toFixed(2)} USDT + {"avg_duration_minutes" in stats && stats.avg_duration_minutes !== null && stats.avg_duration_minutes !== undefined && ( +