a
This commit is contained in:
parent
8612b6cb21
commit
e3b6dfb65d
|
|
@ -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))))
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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'):
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user