From ecc44ade598b8ec8c6220f7d576a9f8f8289fb05 Mon Sep 17 00:00:00 2001 From: TIANHE Date: Thu, 12 Mar 2026 00:56:29 +0800 Subject: [PATCH] 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 --- backend_api_python/app/routes/dashboard.py | 75 +++++++++++++------ .../app/services/trading_executor.py | 21 +++++- 2 files changed, 72 insertions(+), 24 deletions(-) diff --git a/backend_api_python/app/routes/dashboard.py b/backend_api_python/app/routes/dashboard.py index 3a6982a..b3117ee 100644 --- a/backend_api_python/app/routes/dashboard.py +++ b/backend_api_python/app/routes/dashboard.py @@ -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) diff --git a/backend_api_python/app/services/trading_executor.py b/backend_api_python/app/services/trading_executor.py index d01dc22..35652e2 100644 --- a/backend_api_python/app/services/trading_executor.py +++ b/backend_api_python/app/services/trading_executor.py @@ -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)