This commit is contained in:
aaron 2026-02-25 21:06:07 +08:00
parent 64c5ce606e
commit fbdb1cf421
2 changed files with 149 additions and 3 deletions

View File

@ -1099,7 +1099,7 @@ class PaperTradingService:
if end_date:
query = query.filter(PaperOrder.closed_at <= end_date)
orders = query.all()
orders = query.order_by(PaperOrder.closed_at.asc()).all()
if not orders:
return self._empty_statistics()
@ -1118,6 +1118,9 @@ class PaperTradingService:
gross_profit = sum(wins) if wins else 0
gross_loss = sum(losses) if losses else 0
# 计算账户最大回撤
account_max_drawdown = self._calculate_account_max_drawdown(orders)
return {
'total_trades': total_trades,
'winning_trades': winning_trades,
@ -1129,7 +1132,8 @@ class PaperTradingService:
'average_win': round(sum(wins) / len(wins), 2) if wins else 0,
'average_loss': round(sum(losses) / len(losses), 2) if losses else 0,
'profit_factor': round(gross_profit / gross_loss, 2) if gross_loss > 0 else float('inf'),
'max_drawdown': min(o.max_drawdown for o in orders) if orders else 0,
'max_drawdown': round(account_max_drawdown, 2),
'max_drawdown_amount': round(account_max_drawdown * self.initial_balance / 100, 2) if account_max_drawdown != 0 else 0,
'best_trade': max(o.pnl_percent for o in orders) if orders else 0,
'worst_trade': min(o.pnl_percent for o in orders) if orders else 0,
'by_grade': self._calculate_grade_statistics(orders),
@ -1140,6 +1144,49 @@ class PaperTradingService:
finally:
db.close()
def _calculate_account_max_drawdown(self, orders: List[PaperOrder]) -> float:
"""
计算账户最大回撤
账户最大回撤是指在统计周期内账户权益从最高点跌落的最大百分比
这是通过模拟每笔交易平仓后的账户余额来计算的
Args:
orders: 已平仓订单列表应按平仓时间排序
Returns:
账户最大回撤百分比负数 -5.23 表示最大回撤 5.23%
"""
if not orders:
return 0.0
# 确保订单按平仓时间排序
orders = sorted(orders, key=lambda o: o.closed_at or datetime.utcnow())
# 模拟账户权益曲线
initial_balance = self.initial_balance
running_balance = initial_balance
peak_balance = initial_balance
max_drawdown = 0.0
for order in orders:
if order.closed_at and order.pnl_amount is not None:
# 更新账户余额
running_balance += order.pnl_amount
# 检查是否创新高
if running_balance > peak_balance:
peak_balance = running_balance
# 计算从峰值到当前值的回撤
if peak_balance > 0:
drawdown = (running_balance - peak_balance) / peak_balance * 100
# 记录最大回撤(最负的值)
if drawdown < max_drawdown:
max_drawdown = drawdown
return max_drawdown
def _empty_statistics(self) -> Dict[str, Any]:
"""返回空统计结构"""
return {
@ -1154,6 +1201,7 @@ class PaperTradingService:
'average_loss': 0,
'profit_factor': 0,
'max_drawdown': 0,
'max_drawdown_amount': 0,
'best_trade': 0,
'worst_trade': 0,
'by_grade': {},
@ -1185,6 +1233,10 @@ class PaperTradingService:
).all()
realized_pnl = sum(o.pnl_amount for o in closed_orders)
# 计算账户最大回撤
account_max_drawdown = self._calculate_account_max_drawdown(closed_orders)
finally:
db.close()
@ -1210,7 +1262,10 @@ class PaperTradingService:
'max_orders': self.max_orders,
'available_orders': available_orders,
'total_position_value': round(used_margin * self.leverage, 2),
'margin_ratio': round((used_margin / current_balance * 100), 2) if current_balance > 0 else 0
'margin_ratio': round((used_margin / current_balance * 100), 2) if current_balance > 0 else 0,
'max_drawdown': round(account_max_drawdown, 2), # 账户最大回撤百分比
'max_drawdown_amount': round(account_max_drawdown * self.initial_balance / 100, 2) if account_max_drawdown != 0 else 0, # 账户最大回撤金额
'total_pnl_percent': round((realized_pnl / self.initial_balance * 100), 2) if self.initial_balance > 0 else 0
}
def _calculate_grade_statistics(self, orders: List[PaperOrder]) -> Dict[str, Any]:

View File

@ -1087,6 +1087,12 @@
<div class="stat-label">盈亏比</div>
<div class="stat-value">{{ stats.profit_factor === Infinity ? '∞' : stats.profit_factor.toFixed(2) }}</div>
</div>
<div class="stat-card">
<div class="stat-label">最大回撤</div>
<div class="stat-value" :class="stats.max_drawdown <= 0 ? 'negative' : 'positive'">
{{ stats.max_drawdown ? stats.max_drawdown.toFixed(2) : '0.00' }}%
</div>
</div>
</div>
<!-- 实时价格 -->
@ -1267,6 +1273,91 @@
<!-- 详细统计 -->
<div v-if="activeTab === 'stats'">
<!-- 总体指标汇总 -->
<div class="grade-stats" style="margin-bottom: 30px;">
<div class="grade-card">
<div class="grade-card-header">
<span class="grade-card-title">交易统计</span>
</div>
<div class="grade-card-stats">
<div class="grade-stat-row">
<span class="grade-stat-label">总交易数</span>
<span>{{ stats.total_trades }}</span>
</div>
<div class="grade-stat-row">
<span class="grade-stat-label">盈利交易</span>
<span class="positive">{{ stats.winning_trades }}</span>
</div>
<div class="grade-stat-row">
<span class="grade-stat-label">亏损交易</span>
<span class="negative">{{ stats.losing_trades }}</span>
</div>
<div class="grade-stat-row">
<span class="grade-stat-label">胜率</span>
<span>{{ stats.win_rate.toFixed(1) }}%</span>
</div>
<div class="grade-stat-row">
<span class="grade-stat-label">平均盈利</span>
<span class="positive">${{ stats.average_win.toFixed(2) }}</span>
</div>
<div class="grade-stat-row">
<span class="grade-stat-label">平均亏损</span>
<span class="negative">${{ stats.average_loss.toFixed(2) }}</span>
</div>
</div>
</div>
<div class="grade-card">
<div class="grade-card-header">
<span class="grade-card-title">盈亏指标</span>
</div>
<div class="grade-card-stats">
<div class="grade-stat-row">
<span class="grade-stat-label">总盈亏</span>
<span :class="stats.total_pnl >= 0 ? 'positive' : 'negative'">
${{ stats.total_pnl.toFixed(2) }}
</span>
</div>
<div class="grade-stat-row">
<span class="grade-stat-label">平均盈亏</span>
<span :class="stats.average_pnl >= 0 ? 'positive' : 'negative'">
${{ stats.average_pnl.toFixed(2) }}
</span>
</div>
<div class="grade-stat-row">
<span class="grade-stat-label">盈亏比</span>
<span>{{ stats.profit_factor === Infinity ? '∞' : stats.profit_factor.toFixed(2) }}</span>
</div>
<div class="grade-stat-row">
<span class="grade-stat-label">最佳交易</span>
<span class="positive">{{ stats.best_trade.toFixed(2) }}%</span>
</div>
<div class="grade-stat-row">
<span class="grade-stat-label">最差交易</span>
<span class="negative">{{ stats.worst_trade.toFixed(2) }}%</span>
</div>
</div>
</div>
<div class="grade-card">
<div class="grade-card-header">
<span class="grade-card-title">风险指标</span>
</div>
<div class="grade-card-stats">
<div class="grade-stat-row">
<span class="grade-stat-label">最大回撤</span>
<span :class="stats.max_drawdown <= 0 ? 'negative' : 'positive'">
{{ stats.max_drawdown ? stats.max_drawdown.toFixed(2) : '0.00' }}%
</span>
</div>
<div class="grade-stat-row">
<span class="grade-stat-label">回撤金额</span>
<span :class="stats.max_drawdown_amount <= 0 ? 'negative' : 'positive'">
{{ stats.max_drawdown_amount ? (stats.max_drawdown_amount >= 0 ? '+$' : '-$') + Math.abs(stats.max_drawdown_amount).toFixed(2) : '$0.00' }}
</span>
</div>
</div>
</div>
</div>
<h3 style="color: var(--text-primary); font-weight: 300; margin-bottom: 16px;">按信号等级统计</h3>
<div class="grade-stats">
<div class="grade-card" v-for="(data, grade) in stats.by_grade" :key="grade">