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 fastapi import APIRouter, Query, HTTPException
from typing import Optional from typing import Optional
from datetime import datetime, timedelta from datetime import datetime, timedelta
from collections import Counter
import sys import sys
from pathlib import Path from pathlib import Path
import logging 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 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 = { stats = {
"total_trades": len(trades), "total_trades": len(trades),
"closed_trades": len(closed_trades), "closed_trades": len(closed_trades),
@ -243,6 +261,8 @@ async def get_trade_stats(
"avg_loss_pnl_abs": avg_loss_pnl_abs, "avg_loss_pnl_abs": avg_loss_pnl_abs,
"avg_win_loss_ratio": win_loss_ratio, "avg_win_loss_ratio": win_loss_ratio,
"avg_win_loss_ratio_target": 3.0, "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 # 总交易量(名义下单量口径):优先使用 notional_usdt新字段否则回退 entry_price * quantity
"total_notional_usdt": sum( "total_notional_usdt": sum(
float(t.get('notional_usdt') or (float(t.get('entry_price', 0)) * float(t.get('quantity', 0)))) 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 {stats.avg_pnl.toFixed(2)} USDT
</div> </div>
</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 && ( {"avg_win_pnl" in stats && "avg_loss_pnl_abs" in stats && Number(stats.total_pnl || 0) > 0 && (
<div className="stat-card"> <div className="stat-card">
<div className="stat-label">平均盈利 / 平均亏损期望 3:1</div> <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) min_signal_strength = config.TRADING_CONFIG.get('MIN_SIGNAL_STRENGTH', 7)
should_trade = signal_strength >= min_signal_strength and direction is not None 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趋势相反直接拒绝交易 # 如果信号方向与4H趋势相反直接拒绝交易
if direction and trend_4h: if direction and trend_4h:
if (direction == 'BUY' and trend_4h == 'down') or (direction == 'SELL' and trend_4h == 'up'): if (direction == 'BUY' and trend_4h == 'down') or (direction == 'SELL' and trend_4h == 'up'):