TradingAgents/tradingagents/ui/pages/performance.py

187 lines
6.7 KiB
Python

"""
Performance analytics page — strategy comparison and win/loss analysis.
Shows strategy scatter plot with themed Plotly charts, per-strategy
breakdown table, and win rate distribution.
"""
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
import streamlit as st
from tradingagents.ui.theme import COLORS, get_plotly_template, page_header
from tradingagents.ui.utils import load_statistics, load_strategy_metrics
def render() -> None:
"""Render the performance analytics page."""
st.markdown(
page_header("Performance", "Strategy analytics & win/loss breakdown"),
unsafe_allow_html=True,
)
strategy_metrics = load_strategy_metrics()
stats = load_statistics()
if not strategy_metrics:
st.warning(
"No performance data available yet. Run the discovery pipeline and track outcomes."
)
return
template = get_plotly_template()
df = pd.DataFrame(strategy_metrics)
# ---- Summary KPIs ----
total_trades = df["Count"].sum()
avg_wr = (df["Win Rate"] * df["Count"]).sum() / total_trades if total_trades > 0 else 0
avg_ret = (df["Avg Return"] * df["Count"]).sum() / total_trades if total_trades > 0 else 0
n_strategies = len(df)
cols = st.columns(4)
summaries = [
("Total Trades", str(int(total_trades))),
("Weighted Win Rate", f"{avg_wr:.1f}%"),
("Weighted Avg Return", f"{avg_ret:+.2f}%"),
("Active Strategies", str(n_strategies)),
]
for col, (label, val) in zip(cols, summaries):
with col:
st.markdown(
f"""
<div style="background:{COLORS['bg_card']};border:1px solid {COLORS['border']};
border-radius:8px;padding:0.85rem 1rem;text-align:center;">
<div style="font-family:'DM Sans',sans-serif;font-size:0.65rem;
font-weight:600;text-transform:uppercase;letter-spacing:0.06em;
color:{COLORS['text_muted']};margin-bottom:0.3rem;">{label}</div>
<div style="font-family:'JetBrains Mono',monospace;font-size:1.3rem;
font-weight:700;color:{COLORS['text_primary']};">{val}</div>
</div>
""",
unsafe_allow_html=True,
)
st.markdown("<div style='height:1.5rem;'></div>", unsafe_allow_html=True)
# ---- Two-column: scatter + bar chart ----
left_col, right_col = st.columns(2)
with left_col:
st.markdown(
'<div class="section-title">Win Rate vs Return <span class="accent">// scatter</span></div>',
unsafe_allow_html=True,
)
fig = px.scatter(
df,
x="Win Rate",
y="Avg Return",
size="Count",
color="Win Rate",
hover_name="Strategy",
hover_data={"Win Rate": ":.1f", "Avg Return": ":.2f", "Count": True},
labels={"Win Rate": "Win Rate (%)", "Avg Return": "Avg Return (%)"},
color_continuous_scale=[
[0, COLORS["red"]],
[0.5, COLORS["amber"]],
[1.0, COLORS["green"]],
],
size_max=45,
)
fig.add_hline(y=0, line_dash="dot", line_color=COLORS["text_muted"], opacity=0.4)
fig.add_vline(x=50, line_dash="dot", line_color=COLORS["text_muted"], opacity=0.4)
fig.update_layout(
**template,
height=400,
showlegend=False,
coloraxis_showscale=False,
)
st.plotly_chart(fig, width="stretch")
with right_col:
st.markdown(
'<div class="section-title">Win Rate by Strategy <span class="accent">// bar</span></div>',
unsafe_allow_html=True,
)
df_sorted = df.sort_values("Win Rate", ascending=True)
colors = [COLORS["green"] if wr >= 50 else COLORS["red"] for wr in df_sorted["Win Rate"]]
fig_bar = go.Figure(
go.Bar(
x=df_sorted["Win Rate"],
y=df_sorted["Strategy"],
orientation="h",
marker_color=colors,
text=[f"{wr:.0f}%" for wr in df_sorted["Win Rate"]],
textposition="auto",
textfont=dict(family="JetBrains Mono", size=11, color=COLORS["text_primary"]),
)
)
fig_bar.add_vline(x=50, line_dash="dot", line_color=COLORS["text_muted"], opacity=0.5)
fig_bar.update_layout(
**template,
height=400,
xaxis_title="Win Rate (%)",
yaxis_title="",
)
fig_bar.update_yaxes(tickfont=dict(family="JetBrains Mono", size=11))
st.plotly_chart(fig_bar, width="stretch")
# ---- Strategy breakdown table ----
st.markdown("<div style='height:1rem;'></div>", unsafe_allow_html=True)
st.markdown(
'<div class="section-title">Detailed Breakdown <span class="accent">// table</span></div>',
unsafe_allow_html=True,
)
display_df = df.copy()
display_df = display_df.sort_values("Win Rate", ascending=False)
display_df["Count"] = display_df["Count"].astype(int)
st.dataframe(
display_df,
width="stretch",
hide_index=True,
column_config={
"Win Rate": st.column_config.NumberColumn(format="%.1f%%"),
"Avg Return": st.column_config.NumberColumn(format="%+.2f%%"),
"Count": st.column_config.NumberColumn(format="%d"),
},
)
# ---- Per-strategy stats from statistics.json ----
if stats and stats.get("by_strategy"):
st.markdown("<div style='height:1rem;'></div>", unsafe_allow_html=True)
st.markdown(
'<div class="section-title">Time-Period Breakdown <span class="accent">// 1d / 7d / 30d</span></div>',
unsafe_allow_html=True,
)
by_strat = stats["by_strategy"]
rows = []
for strat_name, data in by_strat.items():
rows.append(
{
"Strategy": strat_name,
"Count": data.get("count", 0),
"Win Rate 1d": (
f"{data.get('win_rate_1d', 0):.0f}%" if "win_rate_1d" in data else "N/A"
),
"Win Rate 7d": (
f"{data.get('win_rate_7d', 0):.0f}%" if "win_rate_7d" in data else "N/A"
),
"Wins 1d": data.get("wins_1d", 0),
"Losses 1d": data.get("losses_1d", 0),
"Wins 7d": data.get("wins_7d", 0),
"Losses 7d": data.get("losses_7d", 0),
}
)
if rows:
st.dataframe(pd.DataFrame(rows), width="stretch", hide_index=True)