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 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. 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: { Returns: {
total_trades, winning_trades, losing_trades, win_rate, total_trades, winning_trades, losing_trades, win_rate,
total_profit, total_loss, profit_factor, 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_win = max(profits) if profits else 0.0
max_loss = min(profits) if profits else 0.0 max_loss = min(profits) if profits else 0.0
# Calculate max drawdown from cumulative equity # Calculate max drawdown from equity curve (initial_capital + cumulative profit)
cumulative = [] # This ensures proper percentage calculation even when cumulative profit is negative
acc = 0.0 cumulative_profit = 0.0
equity_curve = []
for p in profits: for p in profits:
acc += p cumulative_profit += p
cumulative.append(acc) 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 max_drawdown = 0.0
for val in cumulative: for equity in equity_curve:
if val > peak: if equity > peak_equity:
peak = val peak_equity = equity
dd = peak - val # Drawdown is the drop from peak
if dd > max_drawdown: drawdown = peak_equity - equity
max_drawdown = dd 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 # Best/worst day
day_profits: Dict[str, float] = {} day_profits: Dict[str, float] = {}
@@ -243,9 +272,9 @@ def _compute_strategy_stats(trades: List[Dict[str, Any]], strategies: List[Dict[
result = [] result = []
for sid, strades in sid_to_trades.items(): 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) 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 roi = (total_pnl / capital * 100) if capital > 0 else 0.0
result.append({ result.append({
@@ -374,13 +403,7 @@ def summary():
trade['created_at'] = int(trade['created_at'].timestamp()) trade['created_at'] = int(trade['created_at'].timestamp())
recent_trades.append(trade) recent_trades.append(trade)
# Compute performance statistics # Total equity/pnl (best-effort) - calculate before performance stats for drawdown calculation
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_initial_capital = 0.0 total_initial_capital = 0.0
for s in strategies: for s in strategies:
try: try:
@@ -388,6 +411,12 @@ def summary():
except Exception: except Exception:
pass 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 # Include realized PnL from trades
total_realized_pnl = sum(_safe_float(t.get("profit"), 0.0) for t in recent_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) 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" pre_import_code = "import numpy as np\nimport pandas as pd\n"
exec(pre_import_code, exec_env) 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 假设已存在 # 这里的 safe_exec_code 假设已存在
exec(indicator_code, exec_env) exec(compatibility_fixed_code, exec_env)
executed_df = exec_env.get('df', df) executed_df = exec_env.get('df', df)