This commit is contained in:
薇薇安 2026-01-19 20:11:47 +08:00
parent 8612b6cb21
commit e3b6dfb65d
3 changed files with 60 additions and 0 deletions

View File

@ -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
@ -227,6 +228,23 @@ async def get_trade_stats(
)
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),
"closed_trades": len(closed_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))))

View File

@ -266,6 +266,39 @@ const TradeList = () => {
{stats.avg_pnl.toFixed(2)} USDT
</div>
</div>
{"avg_duration_minutes" in stats && stats.avg_duration_minutes !== null && stats.avg_duration_minutes !== undefined && (
<div className="stat-card">
<div className="stat-label">平均持仓时长分钟</div>
<div className="stat-value">{Number(stats.avg_duration_minutes || 0).toFixed(0)}</div>
<div style={{ fontSize: '12px', color: '#999', marginTop: '4px' }}>
仅统计有意义交易且需要 duration_minutes 字段
</div>
</div>
)}
{"exit_reason_counts" in stats && stats.exit_reason_counts && (
<div className="stat-card">
<div className="stat-label">平仓原因有意义交易</div>
<div className="stat-value" style={{ fontSize: '1.1rem' }}>
{(() => {
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(' / ') : '—'
})()}
</div>
</div>
)}
{"avg_win_pnl" in stats && "avg_loss_pnl_abs" in stats && Number(stats.total_pnl || 0) > 0 && (
<div className="stat-card">
<div className="stat-label">平均盈利 / 平均亏损期望 3:1</div>

View File

@ -413,6 +413,13 @@ class TradingStrategy:
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:
if (direction == 'BUY' and trend_4h == 'down') or (direction == 'SELL' and trend_4h == 'up'):