From e3b6dfb65dc4f26c515e6dbb2b6d4537117f354f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=96=87=E8=96=87=E5=AE=89?= Date: Mon, 19 Jan 2026 20:11:47 +0800 Subject: [PATCH] a --- backend/api/routes/trades.py | 20 ++++++++++++++++ frontend/src/components/TradeList.jsx | 33 +++++++++++++++++++++++++++ trading_system/strategy.py | 7 ++++++ 3 files changed, 60 insertions(+) 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 && ( +
+
平均持仓时长(分钟)
+
{Number(stats.avg_duration_minutes || 0).toFixed(0)}
+
+ (仅统计“有意义交易”,且需要 duration_minutes 字段) +
+
+ )} + {"exit_reason_counts" in stats && stats.exit_reason_counts && ( +
+
平仓原因(有意义交易)
+
+ {(() => { + const m = stats.exit_reason_counts || {} + const stopLoss = Number(m.stop_loss || 0) + const takeProfit = Number(m.take_profit || 0) + const trailing = Number(m.trailing_stop || 0) + const manual = Number(m.manual || 0) + const sync = Number(m.sync || 0) + const other = Number(m.unknown || 0) + const parts = [] + if (stopLoss) parts.push(`止损 ${stopLoss}`) + if (takeProfit) parts.push(`止盈 ${takeProfit}`) + if (trailing) parts.push(`移动止损 ${trailing}`) + if (manual) parts.push(`手动 ${manual}`) + if (sync) parts.push(`同步 ${sync}`) + if (other) parts.push(`其他 ${other}`) + return parts.length ? parts.join(' / ') : '—' + })()} +
+
+ )} {"avg_win_pnl" in stats && "avg_loss_pnl_abs" in stats && Number(stats.total_pnl || 0) > 0 && (
平均盈利 / 平均亏损(期望 3:1)
diff --git a/trading_system/strategy.py b/trading_system/strategy.py index 0117dd2..4fb389f 100644 --- a/trading_system/strategy.py +++ b/trading_system/strategy.py @@ -412,6 +412,13 @@ class TradingStrategy: # 判断是否应该交易(信号强度 >= 7 才交易,提高胜率) min_signal_strength = config.TRADING_CONFIG.get('MIN_SIGNAL_STRENGTH', 7) should_trade = signal_strength >= min_signal_strength and direction is not None + + # 提升胜率:4H趋势中性时不做自动交易(只保留推荐/观察) + # 经验上,中性趋势下“趋势跟踪”信号更容易被来回扫损,导致胜率显著降低与交易次数激增。 + if trend_4h == 'neutral': + if should_trade: + reasons.append("❌ 4H趋势中性(为提升胜率:仅生成推荐,不自动交易)") + should_trade = False # 如果信号方向与4H趋势相反,直接拒绝交易 if direction and trend_4h: