Skip to content

Commit ecc44ad

Browse files
committed
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
1 parent efae348 commit ecc44ad

File tree

2 files changed

+73
-25
lines changed

2 files changed

+73
-25
lines changed

backend_api_python/app/routes/dashboard.py

Lines changed: 53 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -113,9 +113,12 @@ def _calc_pnl_percent(entry_price: float, size: float, pnl: float, leverage: flo
113113
return 0.0
114114

115115

116-
def _compute_performance_stats(trades: List[Dict[str, Any]]) -> Dict[str, Any]:
116+
def _compute_performance_stats(trades: List[Dict[str, Any]], initial_capital: float = 0.0) -> Dict[str, Any]:
117117
"""
118118
Compute performance statistics from trade history.
119+
Args:
120+
trades: List of trade records
121+
initial_capital: Initial capital for calculating equity curve (default: 0.0, will use cumulative profit peak as baseline)
119122
Returns: {
120123
total_trades, winning_trades, losing_trades, win_rate,
121124
total_profit, total_loss, profit_factor,
@@ -163,23 +166,49 @@ def _compute_performance_stats(trades: List[Dict[str, Any]]) -> Dict[str, Any]:
163166
max_win = max(profits) if profits else 0.0
164167
max_loss = min(profits) if profits else 0.0
165168

166-
# Calculate max drawdown from cumulative equity
167-
cumulative = []
168-
acc = 0.0
169+
# Calculate max drawdown from equity curve (initial_capital + cumulative profit)
170+
# This ensures proper percentage calculation even when cumulative profit is negative
171+
cumulative_profit = 0.0
172+
equity_curve = []
169173
for p in profits:
170-
acc += p
171-
cumulative.append(acc)
174+
cumulative_profit += p
175+
equity = initial_capital + cumulative_profit
176+
equity_curve.append(equity)
172177

173-
peak = 0.0
178+
# Calculate max drawdown from equity curve
179+
peak_equity = initial_capital if initial_capital > 0 else (equity_curve[0] if equity_curve else 0.0)
174180
max_drawdown = 0.0
175-
for val in cumulative:
176-
if val > peak:
177-
peak = val
178-
dd = peak - val
179-
if dd > max_drawdown:
180-
max_drawdown = dd
181-
182-
max_drawdown_pct = (max_drawdown / peak * 100) if peak > 0 else 0.0
181+
for equity in equity_curve:
182+
if equity > peak_equity:
183+
peak_equity = equity
184+
# Drawdown is the drop from peak
185+
drawdown = peak_equity - equity
186+
if drawdown > max_drawdown:
187+
max_drawdown = drawdown
188+
189+
# Calculate drawdown percentage: drawdown / peak_equity * 100
190+
# If peak_equity is 0 or very small, use a fallback calculation
191+
if peak_equity > 0:
192+
max_drawdown_pct = (max_drawdown / peak_equity * 100)
193+
elif initial_capital > 0:
194+
# Fallback: use initial capital as baseline
195+
max_drawdown_pct = (max_drawdown / initial_capital * 100) if initial_capital > 0 else 0.0
196+
else:
197+
# Last resort: if no initial capital and peak is 0, calculate from cumulative profit peak
198+
cumulative = []
199+
acc = 0.0
200+
for p in profits:
201+
acc += p
202+
cumulative.append(acc)
203+
peak_profit = max(cumulative) if cumulative else 0.0
204+
if peak_profit > 0:
205+
max_drawdown_pct = (max_drawdown / peak_profit * 100)
206+
else:
207+
max_drawdown_pct = 0.0
208+
209+
# Cap drawdown percentage at reasonable maximum (e.g., 10000%) to avoid display issues
210+
if max_drawdown_pct > 10000:
211+
max_drawdown_pct = 10000.0
183212

184213
# Best/worst day
185214
day_profits: Dict[str, float] = {}
@@ -243,9 +272,9 @@ def _compute_strategy_stats(trades: List[Dict[str, Any]], strategies: List[Dict[
243272

244273
result = []
245274
for sid, strades in sid_to_trades.items():
246-
stats = _compute_performance_stats(strades)
247-
total_pnl = sum(_safe_float(t.get("profit"), 0.0) for t in strades)
248275
capital = sid_to_capital.get(sid, 0.0)
276+
stats = _compute_performance_stats(strades, initial_capital=capital)
277+
total_pnl = sum(_safe_float(t.get("profit"), 0.0) for t in strades)
249278
roi = (total_pnl / capital * 100) if capital > 0 else 0.0
250279

251280
result.append({
@@ -374,20 +403,20 @@ def _truthy(v: Any) -> bool:
374403
trade['created_at'] = int(trade['created_at'].timestamp())
375404
recent_trades.append(trade)
376405

377-
# Compute performance statistics
378-
perf_stats = _compute_performance_stats(recent_trades)
379-
380-
# Compute per-strategy statistics
381-
strategy_stats = _compute_strategy_stats(recent_trades, strategies)
382-
383-
# Total equity/pnl (best-effort)
406+
# Total equity/pnl (best-effort) - calculate before performance stats for drawdown calculation
384407
total_initial_capital = 0.0
385408
for s in strategies:
386409
try:
387410
total_initial_capital += float(s.get("initial_capital") or 0.0)
388411
except Exception:
389412
pass
390413

414+
# Compute performance statistics with initial capital for proper drawdown calculation
415+
perf_stats = _compute_performance_stats(recent_trades, initial_capital=total_initial_capital)
416+
417+
# Compute per-strategy statistics
418+
strategy_stats = _compute_strategy_stats(recent_trades, strategies)
419+
391420
# Include realized PnL from trades
392421
total_realized_pnl = sum(_safe_float(t.get("profit"), 0.0) for t in recent_trades)
393422
total_pnl = float(total_unrealized_pnl + total_realized_pnl)

backend_api_python/app/services/trading_executor.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1888,8 +1888,27 @@ def safe_import(name, *args, **kwargs):
18881888
pre_import_code = "import numpy as np\nimport pandas as pd\n"
18891889
exec(pre_import_code, exec_env)
18901890

1891+
# 兼容性修复:将旧版pandas的fillna(method=...)语法转换为新版语法
1892+
# pandas 2.0+ 移除了 fillna() 的 method 参数,需要使用 ffill() 或 bfill()
1893+
# 旧语法: df.fillna(method='ffill') 或 df.fillna(method="ffill")
1894+
# 新语法: df.ffill()
1895+
import re
1896+
compatibility_fixed_code = indicator_code
1897+
# 替换 fillna(method='ffill') 或 fillna(method="ffill") 为 ffill()
1898+
compatibility_fixed_code = re.sub(
1899+
r'\.fillna\(\s*method\s*=\s*["\']ffill["\']\s*\)',
1900+
'.ffill()',
1901+
compatibility_fixed_code
1902+
)
1903+
# 替换 fillna(method='bfill') 或 fillna(method="bfill") 为 bfill()
1904+
compatibility_fixed_code = re.sub(
1905+
r'\.fillna\(\s*method\s*=\s*["\']bfill["\']\s*\)',
1906+
'.bfill()',
1907+
compatibility_fixed_code
1908+
)
1909+
18911910
# 这里的 safe_exec_code 假设已存在
1892-
exec(indicator_code, exec_env)
1911+
exec(compatibility_fixed_code, exec_env)
18931912

18941913
executed_df = exec_env.get('df', df)
18951914

0 commit comments

Comments
 (0)