fix: Fix max drawdown calculation and pandas fillna compatibility

- Fix max drawdown calculation in dashboard to use equity curve instead of cumulative profit
- Add pandas fillna(method=...) compatibility fix for older indicator code
- Cap drawdown percentage at 10000% to avoid display issues
This commit is contained in:
TIANHE
2026-03-12 00:56:29 +08:00
parent efae3480bf
commit ecc44ade59
2 changed files with 72 additions and 24 deletions
+52 -23
View File
@@ -113,9 +113,12 @@ def _calc_pnl_percent(entry_price: float, size: float, pnl: float, leverage: flo
return 0.0
def _compute_performance_stats(trades: List[Dict[str, Any]]) -> Dict[str, Any]:
def _compute_performance_stats(trades: List[Dict[str, Any]], initial_capital: float = 0.0) -> Dict[str, Any]:
"""
Compute performance statistics from trade history.
Args:
trades: List of trade records
initial_capital: Initial capital for calculating equity curve (default: 0.0, will use cumulative profit peak as baseline)
Returns: {
total_trades, winning_trades, losing_trades, win_rate,
total_profit, total_loss, profit_factor,
@@ -163,23 +166,49 @@ def _compute_performance_stats(trades: List[Dict[str, Any]]) -> Dict[str, Any]:
max_win = max(profits) if profits else 0.0
max_loss = min(profits) if profits else 0.0
# Calculate max drawdown from cumulative equity
cumulative = []
acc = 0.0
# Calculate max drawdown from equity curve (initial_capital + cumulative profit)
# This ensures proper percentage calculation even when cumulative profit is negative
cumulative_profit = 0.0
equity_curve = []
for p in profits:
acc += p
cumulative.append(acc)
cumulative_profit += p
equity = initial_capital + cumulative_profit
equity_curve.append(equity)
peak = 0.0
# Calculate max drawdown from equity curve
peak_equity = initial_capital if initial_capital > 0 else (equity_curve[0] if equity_curve else 0.0)
max_drawdown = 0.0
for val in cumulative:
if val > peak:
peak = val
dd = peak - val
if dd > max_drawdown:
max_drawdown = dd
for equity in equity_curve:
if equity > peak_equity:
peak_equity = equity
# Drawdown is the drop from peak
drawdown = peak_equity - equity
if drawdown > max_drawdown:
max_drawdown = drawdown
max_drawdown_pct = (max_drawdown / peak * 100) if peak > 0 else 0.0
# Calculate drawdown percentage: drawdown / peak_equity * 100
# If peak_equity is 0 or very small, use a fallback calculation
if peak_equity > 0:
max_drawdown_pct = (max_drawdown / peak_equity * 100)
elif initial_capital > 0:
# Fallback: use initial capital as baseline
max_drawdown_pct = (max_drawdown / initial_capital * 100) if initial_capital > 0 else 0.0
else:
# Last resort: if no initial capital and peak is 0, calculate from cumulative profit peak
cumulative = []
acc = 0.0
for p in profits:
acc += p
cumulative.append(acc)
peak_profit = max(cumulative) if cumulative else 0.0
if peak_profit > 0:
max_drawdown_pct = (max_drawdown / peak_profit * 100)
else:
max_drawdown_pct = 0.0
# Cap drawdown percentage at reasonable maximum (e.g., 10000%) to avoid display issues
if max_drawdown_pct > 10000:
max_drawdown_pct = 10000.0
# Best/worst day
day_profits: Dict[str, float] = {}
@@ -243,9 +272,9 @@ def _compute_strategy_stats(trades: List[Dict[str, Any]], strategies: List[Dict[
result = []
for sid, strades in sid_to_trades.items():
stats = _compute_performance_stats(strades)
total_pnl = sum(_safe_float(t.get("profit"), 0.0) for t in strades)
capital = sid_to_capital.get(sid, 0.0)
stats = _compute_performance_stats(strades, initial_capital=capital)
total_pnl = sum(_safe_float(t.get("profit"), 0.0) for t in strades)
roi = (total_pnl / capital * 100) if capital > 0 else 0.0
result.append({
@@ -374,13 +403,7 @@ def summary():
trade['created_at'] = int(trade['created_at'].timestamp())
recent_trades.append(trade)
# Compute performance statistics
perf_stats = _compute_performance_stats(recent_trades)
# Compute per-strategy statistics
strategy_stats = _compute_strategy_stats(recent_trades, strategies)
# Total equity/pnl (best-effort)
# Total equity/pnl (best-effort) - calculate before performance stats for drawdown calculation
total_initial_capital = 0.0
for s in strategies:
try:
@@ -388,6 +411,12 @@ def summary():
except Exception:
pass
# Compute performance statistics with initial capital for proper drawdown calculation
perf_stats = _compute_performance_stats(recent_trades, initial_capital=total_initial_capital)
# Compute per-strategy statistics
strategy_stats = _compute_strategy_stats(recent_trades, strategies)
# Include realized PnL from trades
total_realized_pnl = sum(_safe_float(t.get("profit"), 0.0) for t in recent_trades)
total_pnl = float(total_unrealized_pnl + total_realized_pnl)
@@ -1888,8 +1888,27 @@ class TradingExecutor:
pre_import_code = "import numpy as np\nimport pandas as pd\n"
exec(pre_import_code, exec_env)
# 兼容性修复:将旧版pandas的fillna(method=...)语法转换为新版语法
# pandas 2.0+ 移除了 fillna() 的 method 参数,需要使用 ffill() 或 bfill()
# 旧语法: df.fillna(method='ffill') 或 df.fillna(method="ffill")
# 新语法: df.ffill()
import re
compatibility_fixed_code = indicator_code
# 替换 fillna(method='ffill') 或 fillna(method="ffill") 为 ffill()
compatibility_fixed_code = re.sub(
r'\.fillna\(\s*method\s*=\s*["\']ffill["\']\s*\)',
'.ffill()',
compatibility_fixed_code
)
# 替换 fillna(method='bfill') 或 fillna(method="bfill") 为 bfill()
compatibility_fixed_code = re.sub(
r'\.fillna\(\s*method\s*=\s*["\']bfill["\']\s*\)',
'.bfill()',
compatibility_fixed_code
)
# 这里的 safe_exec_code 假设已存在
exec(indicator_code, exec_env)
exec(compatibility_fixed_code, exec_env)
executed_df = exec_env.get('df', df)