backtrader.bokeh.tabs.performance 源代码
#!/usr/bin/env python
"""
Performance metrics tab.
Displays key performance metrics of the strategy.
"""
from ..tab import BokehTab
try:
from bokeh.layouts import column
from bokeh.models import ColumnDataSource, DataTable, TableColumn
from bokeh.models.widgets import Div
BOKEH_AVAILABLE = True
except ImportError:
BOKEH_AVAILABLE = False
[文档]
class PerformanceTab(BokehTab):
"""Performance metrics tab.
Displays key performance metrics of the strategy, including:
- Total return
- Annual return
- Sharpe ratio
- Maximum drawdown
- Win rate
- Profit/loss ratio
- Trade statistics
"""
def _is_useable(self):
"""Useable when strategy exists."""
if not BOKEH_AVAILABLE:
return False
return self.strategy is not None
def _get_panel(self):
"""Get panel content.
Returns:
tuple: (widget, title)
"""
strategy = self.strategy
scheme = self.scheme
# Get theme colors
title_color = scheme.text_color if scheme else "#333333"
widgets = []
# Title
widgets.append(
Div(
text=f'<h2 style="color: {title_color}; margin-bottom: 20px;">Performance Metrics</h2>',
sizing_mode="stretch_width",
)
)
# Collect performance data
metrics = self._collect_metrics(strategy)
# Create summary cards
summary_html = self._create_summary_cards(metrics, scheme)
widgets.append(Div(text=summary_html, sizing_mode="stretch_width"))
# Returns metrics table
returns_data = self._get_returns_metrics(metrics)
if returns_data:
widgets.append(
Div(
text=f'<h3 style="color: {title_color}; margin-top: 20px;">Returns</h3>',
sizing_mode="stretch_width",
)
)
widgets.append(self._create_metrics_table(returns_data))
# Risk metrics table
risk_data = self._get_risk_metrics(metrics)
if risk_data:
widgets.append(
Div(
text=f'<h3 style="color: {title_color}; margin-top: 20px;">Risk</h3>',
sizing_mode="stretch_width",
)
)
widgets.append(self._create_metrics_table(risk_data))
# Trade statistics table
trade_data = self._get_trade_metrics(metrics)
if trade_data:
widgets.append(
Div(
text=f'<h3 style="color: {title_color}; margin-top: 20px;">Trade Statistics</h3>',
sizing_mode="stretch_width",
)
)
widgets.append(self._create_metrics_table(trade_data))
content = column(*widgets, sizing_mode="stretch_width")
return content, "Performance"
def _collect_metrics(self, strategy):
"""Collect all performance metrics.
Args:
strategy: Strategy instance
Returns:
dict: Performance metrics dictionary
"""
metrics = {}
# Get metrics from analyzers
for analyzer in getattr(strategy, "analyzers", []):
analyzer_name = analyzer.__class__.__name__
try:
analysis = analyzer.get_analysis()
if analyzer_name == "SharpeRatio":
metrics["sharpe_ratio"] = analysis.get("sharperatio", None)
elif analyzer_name == "DrawDown":
analysis.get("drawdown", {})
metrics["max_drawdown"] = analysis.get("max", {}).get("drawdown", None)
metrics["max_drawdown_len"] = analysis.get("max", {}).get("len", None)
elif analyzer_name == "TradeAnalyzer":
total = analysis.get("total", {})
metrics["total_trades"] = total.get("total", 0)
metrics["total_open"] = total.get("open", 0)
metrics["total_closed"] = total.get("closed", 0)
won = analysis.get("won", {})
lost = analysis.get("lost", {})
metrics["won_trades"] = won.get("total", 0)
metrics["lost_trades"] = lost.get("total", 0)
if metrics["total_closed"] > 0:
metrics["win_rate"] = metrics["won_trades"] / metrics["total_closed"] * 100
pnl = analysis.get("pnl", {})
metrics["gross_pnl"] = pnl.get("gross", {}).get("total", None)
metrics["net_pnl"] = pnl.get("net", {}).get("total", None)
streak = analysis.get("streak", {})
metrics["max_win_streak"] = streak.get("won", {}).get("longest", 0)
metrics["max_lose_streak"] = streak.get("lost", {}).get("longest", 0)
elif analyzer_name == "AnnualReturn":
if analysis:
# Calculate average annual return
returns = list(analysis.values())
if returns:
metrics["annual_returns"] = analysis
metrics["avg_annual_return"] = sum(returns) / len(returns) * 100
elif analyzer_name == "SQN":
metrics["sqn"] = analysis.get("sqn", None)
elif analyzer_name == "TimeReturn":
if analysis:
returns = list(analysis.values())
if returns:
total_return = 1
for r in returns:
total_return *= 1 + r
metrics["total_return"] = (total_return - 1) * 100
except Exception:
pass
# Get capital info from broker
if hasattr(strategy, "broker"):
try:
broker = strategy.broker
start_cash = getattr(broker, "startingcash", 100000)
end_value = broker.getvalue()
metrics["start_cash"] = start_cash
metrics["end_value"] = end_value
if "total_return" not in metrics and start_cash > 0:
metrics["total_return"] = (end_value - start_cash) / start_cash * 100
except Exception:
pass
return metrics
def _create_summary_cards(self, metrics, scheme):
"""Create summary cards HTML.
Args:
metrics: Metrics dictionary
scheme: Theme
Returns:
str: HTML string
"""
bg_color = scheme.body_background_color if scheme else "#f5f5f5"
text_color = scheme.text_color if scheme else "#333"
cards = []
# Total return
total_return = metrics.get("total_return")
if total_return is not None:
color = "#4caf50" if total_return >= 0 else "#f44336"
cards.append(f"""
<div style="background: {bg_color}; padding: 15px; border-radius: 8px; text-align: center; min-width: 150px;">
<div style="color: {text_color}; font-size: 12px; opacity: 0.8;">Total Return</div>
<div style="color: {color}; font-size: 24px; font-weight: bold;">{total_return:+.2f}%</div>
</div>
""")
# Sharpe ratio
sharpe = metrics.get("sharpe_ratio")
if sharpe is not None:
color = "#4caf50" if sharpe >= 1 else ("#ff9800" if sharpe >= 0 else "#f44336")
cards.append(f"""
<div style="background: {bg_color}; padding: 15px; border-radius: 8px; text-align: center; min-width: 150px;">
<div style="color: {text_color}; font-size: 12px; opacity: 0.8;">Sharpe Ratio</div>
<div style="color: {color}; font-size: 24px; font-weight: bold;">{sharpe:.2f}</div>
</div>
""")
# Maximum drawdown
max_dd = metrics.get("max_drawdown")
if max_dd is not None:
color = "#4caf50" if max_dd < 10 else ("#ff9800" if max_dd < 20 else "#f44336")
cards.append(f"""
<div style="background: {bg_color}; padding: 15px; border-radius: 8px; text-align: center; min-width: 150px;">
<div style="color: {text_color}; font-size: 12px; opacity: 0.8;">Max Drawdown</div>
<div style="color: {color}; font-size: 24px; font-weight: bold;">{max_dd:.2f}%</div>
</div>
""")
# Win rate
win_rate = metrics.get("win_rate")
if win_rate is not None:
color = "#4caf50" if win_rate >= 50 else "#f44336"
cards.append(f"""
<div style="background: {bg_color}; padding: 15px; border-radius: 8px; text-align: center; min-width: 150px;">
<div style="color: {text_color}; font-size: 12px; opacity: 0.8;">Win Rate</div>
<div style="color: {color}; font-size: 24px; font-weight: bold;">{win_rate:.1f}%</div>
</div>
""")
# Total trades
total_trades = metrics.get("total_trades", 0)
cards.append(f"""
<div style="background: {bg_color}; padding: 15px; border-radius: 8px; text-align: center; min-width: 150px;">
<div style="color: {text_color}; font-size: 12px; opacity: 0.8;">Total Trades</div>
<div style="color: {text_color}; font-size: 24px; font-weight: bold;">{total_trades}</div>
</div>
""")
html = f"""
<div style="display: flex; flex-wrap: wrap; gap: 15px; margin-bottom: 20px;">
{''.join(cards)}
</div>
"""
return html
def _get_returns_metrics(self, metrics):
"""Get returns-related metrics.
Returns:
dict: Metrics dictionary
"""
data = {}
if "start_cash" in metrics:
data["Starting Capital"] = f"${metrics['start_cash']:,.2f}"
if "end_value" in metrics:
data["Ending Value"] = f"${metrics['end_value']:,.2f}"
if "total_return" in metrics:
data["Total Return"] = f"{metrics['total_return']:+.2f}%"
if "avg_annual_return" in metrics:
data["Avg Annual Return"] = f"{metrics['avg_annual_return']:+.2f}%"
if "net_pnl" in metrics and metrics["net_pnl"] is not None:
data["Net P&L"] = f"${metrics['net_pnl']:,.2f}"
return data
def _get_risk_metrics(self, metrics):
"""Get risk-related metrics.
Returns:
dict: Metrics dictionary
"""
data = {}
if "sharpe_ratio" in metrics and metrics["sharpe_ratio"] is not None:
data["Sharpe Ratio"] = f"{metrics['sharpe_ratio']:.3f}"
if "sqn" in metrics and metrics["sqn"] is not None:
data["SQN"] = f"{metrics['sqn']:.2f}"
if "max_drawdown" in metrics and metrics["max_drawdown"] is not None:
data["Max Drawdown"] = f"{metrics['max_drawdown']:.2f}%"
if "max_drawdown_len" in metrics and metrics["max_drawdown_len"] is not None:
data["Max DD Duration"] = f"{metrics['max_drawdown_len']} bars"
return data
def _get_trade_metrics(self, metrics):
"""Get trade statistics metrics.
Returns:
dict: Metrics dictionary
"""
data = {}
if "total_trades" in metrics:
data["Total Trades"] = str(metrics["total_trades"])
if "total_closed" in metrics:
data["Closed Trades"] = str(metrics["total_closed"])
if "total_open" in metrics:
data["Open Trades"] = str(metrics["total_open"])
if "won_trades" in metrics:
data["Winning Trades"] = str(metrics["won_trades"])
if "lost_trades" in metrics:
data["Losing Trades"] = str(metrics["lost_trades"])
if "win_rate" in metrics:
data["Win Rate"] = f"{metrics['win_rate']:.1f}%"
if "max_win_streak" in metrics:
data["Max Win Streak"] = str(metrics["max_win_streak"])
if "max_lose_streak" in metrics:
data["Max Lose Streak"] = str(metrics["max_lose_streak"])
return data
def _create_metrics_table(self, data):
"""Create metrics table.
Args:
data: Metrics dictionary
Returns:
DataTable
"""
source = ColumnDataSource(data={"metric": list(data.keys()), "value": list(data.values())})
columns = [
TableColumn(field="metric", title="Metric", width=200),
TableColumn(field="value", title="Value", width=150),
]
table = DataTable(
source=source,
columns=columns,
width=400,
height=min(len(data) * 28 + 30, 300),
index_position=None,
)
return table