
Trading Visualization
- 250 installs
- 257 repo stars
- Updated June 24, 2026
- agiprolabs/claude-trading-skills
trading-visualization is a Claude Code skill that generates trading charts (candlesticks, equity curves, drawdowns, heatmaps) in Python using mplfinance, matplotlib, and plotly.
About
A Claude Code skill for producing trading charts in Python. It covers candlestick charts, equity curves, drawdown plots, return distributions, and correlation heatmaps using mplfinance, matplotlib, and plotly with a dark-theme trading style. Developers use it to visualize price action and evaluate whether a strategy is robust or curve-fit.
- Generates candlesticks, equity curves, drawdowns, and correlation heatmaps
- Covers mplfinance, matplotlib, and plotly with a dark-theme default
- Multi-panel layouts for price, volume, and indicator stacks
Trading Visualization by the numbers
- 250 all-time installs (skills.sh)
- Ranked #361 of 1,106 Finance & Trading skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
trading-visualization capabilities & compatibility
Free; runs locally in Python with open-source charting libraries
- Capabilities
- trading visualization · chart generation · equity curve plotting · drawdown analysis · correlation heatmap
- Use cases
- data analysis · trading
- Pricing
- Free
What trading-visualization says it does
Visualization is the primary interface between a trader and their data.
Equity curves, drawdown plots, and return distributions expose whether a strategy is robust or curve-fit.
Trading terminals use dark backgrounds by default. All charts in this skill follow that convention.
npx skills add https://github.com/agiprolabs/claude-trading-skills --skill trading-visualizationAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 250 |
|---|---|
| repo stars | ★ 257 |
| Last updated | June 24, 2026 |
| Repository | agiprolabs/claude-trading-skills ↗ |
What it does
Generate trading charts like candlesticks, equity curves, and drawdowns to evaluate a strategy or report performance.
Who is it for?
Producing publication-quality trading charts to evaluate strategies and report performance.
Skip if: Backtesting logic itself or non-financial data visualization.
When should I use this skill?
You need to chart price action, an equity curve, or a drawdown for a trading strategy.
What you get
Dark-theme trading charts for pattern recognition, strategy evaluation, or reporting.
- Trading charts such as candlesticks, equity curves, drawdowns, and heatmaps
By the numbers
- 8 chart types covered
- 3 charting libraries (mplfinance, matplotlib, plotly)
Files
Trading Visualization
Visualization is the primary interface between a trader and their data. Charts reveal patterns that tables and numbers cannot: breakdowns in strategy, regime transitions, clustering of losses, and the shape of risk. A well-designed chart communicates more in a glance than a page of statistics.
Three uses of trading charts:
1. Pattern recognition — Spot structural changes in price, volume, and momentum that quantitative filters miss. 2. Strategy evaluation — Equity curves, drawdown plots, and return distributions expose whether a strategy is robust or curve-fit. 3. Reporting — Communicate performance to stakeholders, journals, or your future self with publication-quality visuals.
---
Chart Types Covered
| Chart Type | Purpose | Library |
|---|---|---|
| Candlestick | OHLCV price action with overlays | mplfinance |
| Equity curve | Portfolio value over time | matplotlib |
| Drawdown | Underwater equity plot | matplotlib |
| Return distribution | Histogram + normal fit | matplotlib |
| Correlation heatmap | Cross-asset correlation matrix | matplotlib / seaborn |
| Trade markers | Entry/exit points on price chart | mplfinance / matplotlib |
| Indicator panels | RSI, MACD below price chart | mplfinance |
| Position timeline | When positions were held | matplotlib |
---
Libraries
mplfinance
Best for candlestick charts. Built on matplotlib with finance-specific defaults.
uv pip install mplfinanceimport mplfinance as mpf
# Basic candlestick from a DataFrame with DatetimeIndex
# Columns: Open, High, Low, Close, Volume
mpf.plot(df, type="candle", volume=True, style="charles")Key features:
- Native OHLCV support — pass a DataFrame directly
- Built-in volume bars
addplotfor overlays (moving averages, Bollinger Bands)- Custom styles via
mpf.make_mpf_style()
matplotlib
General purpose, most flexible. Use when you need full control over layout.
uv pip install matplotlibimport matplotlib.pyplot as plt
fig, axes = plt.subplots(2, 1, figsize=(14, 8), height_ratios=[3, 1],
sharex=True)
axes[0].plot(dates, equity, color="#00ff88")
axes[1].fill_between(dates, drawdown, 0, color="#ff4444", alpha=0.5)plotly
Interactive charts rendered as HTML. Best for exploration and dashboards.
uv pip install plotlyimport plotly.graph_objects as go
fig = go.Figure(data=[go.Candlestick(
x=df.index, open=df["Open"], high=df["High"],
low=df["Low"], close=df["Close"]
)])
fig.update_layout(template="plotly_dark")
fig.write_html("chart.html")---
Styling: Dark Theme Default
Trading terminals use dark backgrounds by default. All charts in this skill follow that convention.
Quick dark theme setup
import matplotlib.pyplot as plt
plt.style.use("dark_background")
plt.rcParams.update({
"figure.facecolor": "#1a1a2e",
"axes.facecolor": "#1a1a2e",
"axes.edgecolor": "#333333",
"grid.color": "#333333",
"grid.alpha": 0.4,
"text.color": "#e0e0e0",
"xtick.color": "#aaaaaa",
"ytick.color": "#aaaaaa",
})Trading color scheme
| Element | Color | Hex |
|---|---|---|
| Bullish / profit | Green | #00ff88 |
| Bearish / loss | Red | #ff4444 |
| Neutral / info | Blue | #4488ff |
| Warning | Amber | #ffaa00 |
| MA short | Orange | #ff6600 |
| MA long | Blue | #3399ff |
| MA signal | Yellow | #ffcc00 |
See references/styling_guide.md for complete typography, layout ratios, and export settings.
---
Chart Composition: Multi-Panel Layout
Most trading charts need multiple synchronized panels — price on top, volume in the middle, indicators at the bottom.
Stacked panels with shared x-axis
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
fig = plt.figure(figsize=(14, 10))
gs = gridspec.GridSpec(3, 1, height_ratios=[3, 1, 1], hspace=0.05)
ax_price = fig.add_subplot(gs[0])
ax_volume = fig.add_subplot(gs[1], sharex=ax_price)
ax_rsi = fig.add_subplot(gs[2], sharex=ax_price)
# Hide x-tick labels on upper panels
ax_price.tick_params(labelbottom=False)
ax_volume.tick_params(labelbottom=False)Panel height ratios
| Layout | Ratios | Use Case |
|---|---|---|
| Price + Volume | [3, 1] | Simple OHLCV chart |
| Price + Volume + Indicator | [3, 1, 1] | Standard analysis view |
| Equity + Drawdown | [2, 1] | Performance review |
| Price + RSI + MACD | [3, 1, 1] | Full indicator stack |
---
Candlestick Charts with Overlays
import mplfinance as mpf
import pandas as pd
# df: DataFrame with DatetimeIndex, columns Open/High/Low/Close/Volume
ema20 = df["Close"].ewm(span=20).mean()
ema50 = df["Close"].ewm(span=50).mean()
ap = [
mpf.make_addplot(ema20, color="#ff6600", width=1.2),
mpf.make_addplot(ema50, color="#3399ff", width=1.2),
]
style = mpf.make_mpf_style(
base_mpf_style="nightclouds",
marketcolors=mpf.make_marketcolors(
up="#00ff88", down="#ff4444",
wick={"up": "#00ff88", "down": "#ff4444"},
edge={"up": "#00ff88", "down": "#ff4444"},
volume={"up": "#00ff88", "down": "#ff4444"},
),
facecolor="#1a1a2e", figcolor="#1a1a2e",
gridcolor="#333333", gridstyle="--",
)
mpf.plot(df, type="candle", style=style, addplot=ap,
volume=True, figsize=(14, 8),
title="Token / SOL — 15m", savefig="candles.png")---
Equity Curve with Drawdown Panel
import numpy as np
import matplotlib.pyplot as plt
def plot_equity_drawdown(equity: pd.Series, title: str = "Portfolio") -> plt.Figure:
"""Plot equity curve with drawdown panel below."""
peak = equity.cummax()
drawdown = (equity - peak) / peak
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(14, 8),
height_ratios=[2, 1], sharex=True)
ax1.plot(equity.index, equity, color="#00ff88", linewidth=1.5)
ax1.plot(equity.index, peak, color="#555555", linewidth=0.8,
linestyle="--", label="Peak")
ax1.set_title(title, fontsize=14, fontweight="bold", color="white")
ax1.set_ylabel("Portfolio Value", fontsize=11)
ax1.legend(loc="upper left")
ax1.grid(True, alpha=0.3)
ax2.fill_between(equity.index, drawdown, 0, color="#ff4444", alpha=0.5)
ax2.set_ylabel("Drawdown", fontsize=11)
ax2.set_xlabel("Date", fontsize=11)
ax2.grid(True, alpha=0.3)
fig.tight_layout()
return fig---
Return Distribution
from scipy import stats
def plot_return_distribution(returns: pd.Series) -> plt.Figure:
"""Histogram of returns with normal fit and risk metrics."""
fig, ax = plt.subplots(figsize=(10, 6))
ax.hist(returns, bins=50, density=True, alpha=0.7,
color="#4488ff", edgecolor="#333333")
# Normal fit overlay
mu, sigma = returns.mean(), returns.std()
x = np.linspace(returns.min(), returns.max(), 200)
ax.plot(x, stats.norm.pdf(x, mu, sigma), color="#ffaa00",
linewidth=2, label=f"Normal(μ={mu:.4f}, σ={sigma:.4f})")
# VaR line
var_95 = returns.quantile(0.05)
ax.axvline(var_95, color="#ff4444", linestyle="--",
label=f"VaR 95%: {var_95:.4f}")
ax.set_title("Return Distribution", fontsize=14, fontweight="bold")
ax.set_xlabel("Return", fontsize=11)
ax.legend()
ax.grid(True, alpha=0.3)
fig.tight_layout()
return fig---
Correlation Heatmap
def plot_correlation_heatmap(returns_df: pd.DataFrame) -> plt.Figure:
"""Correlation matrix heatmap with annotations."""
corr = returns_df.corr()
fig, ax = plt.subplots(figsize=(10, 8))
im = ax.imshow(corr, cmap="RdYlGn", vmin=-1, vmax=1, aspect="auto")
ax.set_xticks(range(len(corr.columns)))
ax.set_yticks(range(len(corr.columns)))
ax.set_xticklabels(corr.columns, rotation=45, ha="right")
ax.set_yticklabels(corr.columns)
for i in range(len(corr)):
for j in range(len(corr)):
ax.text(j, i, f"{corr.iloc[i, j]:.2f}",
ha="center", va="center", fontsize=9,
color="black" if abs(corr.iloc[i, j]) < 0.5 else "white")
fig.colorbar(im, ax=ax, shrink=0.8)
ax.set_title("Correlation Matrix", fontsize=14, fontweight="bold")
fig.tight_layout()
return fig---
Trade Markers on Price Chart
def plot_trades_on_price(
price: pd.Series,
entries: pd.DataFrame, # columns: date, price, side
exits: pd.DataFrame, # columns: date, price, pnl
) -> plt.Figure:
"""Price chart with entry/exit markers."""
fig, ax = plt.subplots(figsize=(14, 7))
ax.plot(price.index, price, color="#aaaaaa", linewidth=1)
# Entry markers
buy_mask = entries["side"] == "long"
ax.scatter(entries.loc[buy_mask, "date"], entries.loc[buy_mask, "price"],
marker="^", color="#00ff88", s=100, zorder=5, label="Buy")
ax.scatter(entries.loc[~buy_mask, "date"], entries.loc[~buy_mask, "price"],
marker="v", color="#ff4444", s=100, zorder=5, label="Short")
# Exit markers
win_mask = exits["pnl"] > 0
ax.scatter(exits.loc[win_mask, "date"], exits.loc[win_mask, "price"],
marker="x", color="#00ff88", s=80, zorder=5)
ax.scatter(exits.loc[~win_mask, "date"], exits.loc[~win_mask, "price"],
marker="x", color="#ff4444", s=80, zorder=5)
ax.set_title("Trades on Price", fontsize=14, fontweight="bold")
ax.legend()
ax.grid(True, alpha=0.3)
fig.tight_layout()
return fig---
Output Formats
| Format | Method | Use Case |
|---|---|---|
| PNG | fig.savefig("chart.png", dpi=150) | Sharing, embedding |
| SVG | fig.savefig("chart.svg") | Editing, scaling |
| HTML | fig.write_html("chart.html") (plotly) | Interactive exploration |
| Inline | plt.show() | Jupyter notebooks |
Saving with dark background
fig.savefig("chart.png", dpi=150, facecolor=fig.get_facecolor(),
edgecolor="none", bbox_inches="tight")---
Integration with Other Skills
| Skill | Integration |
|---|---|
pandas-ta | Compute indicators, pass to addplot overlays |
vectorbt | Extract equity curve and trade list for visualization |
portfolio-analytics | Plot Sharpe, drawdown, and return metrics |
risk-management | Visualize position limits and exposure over time |
position-sizing | Chart position size vs account equity over time |
regime-detection | Color background by detected market regime |
correlation-analysis | Generate correlation heatmaps from return data |
---
Files
References
references/chart_recipes.md— Complete code recipes for six common chart typesreferences/styling_guide.md— Dark theme setup, colors, typography, layout, and export settings
Scripts
scripts/chart_generator.py— Generate four chart types from synthetic data (candlestick, equity, returns, trades)scripts/performance_report.py— Multi-chart performance report with summary statistics
Chart Recipes
Copy-paste-ready recipes for six common trading chart types with dark theme styling.
Recipe 1: Candlestick with Moving Averages
import pandas as pd
import mplfinance as mpf
def candlestick_with_ma(df: pd.DataFrame, short: int = 20, long: int = 50,
save_path: str = "candles.png") -> None:
"""Candlestick chart with EMA overlays and volume bars."""
ema_short = df["Close"].ewm(span=short).mean()
ema_long = df["Close"].ewm(span=long).mean()
ap = [
mpf.make_addplot(ema_short, color="#ff6600", width=1.2,
label=f"EMA {short}"),
mpf.make_addplot(ema_long, color="#3399ff", width=1.2,
label=f"EMA {long}"),
]
mc = mpf.make_marketcolors(
up="#00ff88", down="#ff4444",
wick={"up": "#00ff88", "down": "#ff4444"},
edge={"up": "#00ff88", "down": "#ff4444"},
volume={"up": "#00ff88", "down": "#ff4444"},
)
style = mpf.make_mpf_style(
base_mpf_style="nightclouds", marketcolors=mc,
facecolor="#1a1a2e", figcolor="#1a1a2e",
gridcolor="#333333", gridstyle="--",
)
mpf.plot(df, type="candle", style=style, addplot=ap,
volume=True, figsize=(14, 8),
title="\nCandlestick with EMAs",
savefig=dict(fname=save_path, dpi=150, facecolor="#1a1a2e"))---
Recipe 2: Equity Curve with Drawdown
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
def equity_with_drawdown(equity: pd.Series,
save_path: str = "equity.png") -> plt.Figure:
"""Two-panel chart: equity on top, drawdown filled area below."""
plt.style.use("dark_background")
peak = equity.cummax()
dd = (equity - peak) / peak
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(14, 8),
height_ratios=[2, 1], sharex=True)
fig.patch.set_facecolor("#1a1a2e")
for ax in (ax1, ax2):
ax.set_facecolor("#1a1a2e")
ax1.plot(equity.index, equity, color="#00ff88", linewidth=1.5,
label="Equity")
ax1.plot(equity.index, peak, color="#555555", linewidth=0.8,
linestyle="--", label="Peak")
ax1.set_ylabel("Portfolio Value", fontsize=11)
ax1.set_title("Equity Curve", fontsize=14, fontweight="bold")
ax1.legend(loc="upper left")
ax1.grid(True, alpha=0.3, color="#333333")
ax2.fill_between(equity.index, dd, 0, color="#ff4444", alpha=0.5)
ax2.plot(equity.index, dd, color="#ff4444", linewidth=0.8)
ax2.set_ylabel("Drawdown", fontsize=11)
ax2.set_xlabel("Date", fontsize=11)
ax2.grid(True, alpha=0.3, color="#333333")
fig.tight_layout()
fig.savefig(save_path, dpi=150, facecolor="#1a1a2e",
edgecolor="none", bbox_inches="tight")
return fig---
Recipe 3: Multi-Indicator Panel (Price + RSI + MACD)
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
def multi_indicator_panel(df: pd.DataFrame,
save_path: str = "indicators.png") -> plt.Figure:
"""Three-panel chart: price with MAs, RSI, and MACD."""
plt.style.use("dark_background")
close = df["Close"]
# Compute indicators
ema12 = close.ewm(span=12).mean()
ema26 = close.ewm(span=26).mean()
macd_line = ema12 - ema26
signal_line = macd_line.ewm(span=9).mean()
macd_hist = macd_line - signal_line
delta = close.diff()
gain = delta.clip(lower=0).rolling(14).mean()
loss = (-delta.clip(upper=0)).rolling(14).mean()
rs = gain / loss.replace(0, np.nan)
rsi = 100 - (100 / (1 + rs))
fig = plt.figure(figsize=(14, 10))
fig.patch.set_facecolor("#1a1a2e")
gs = gridspec.GridSpec(3, 1, height_ratios=[3, 1, 1], hspace=0.05)
ax1 = fig.add_subplot(gs[0])
ax2 = fig.add_subplot(gs[1], sharex=ax1)
ax3 = fig.add_subplot(gs[2], sharex=ax1)
for ax in (ax1, ax2, ax3):
ax.set_facecolor("#1a1a2e")
ax.grid(True, alpha=0.3, color="#333333")
# Price panel
ax1.plot(close.index, close, color="#e0e0e0", linewidth=1.2)
ax1.plot(close.index, close.rolling(20).mean(), color="#ff6600",
linewidth=1, label="SMA 20")
ax1.set_ylabel("Price", fontsize=11)
ax1.legend(loc="upper left", fontsize=9)
ax1.tick_params(labelbottom=False)
# RSI panel
ax2.plot(rsi.index, rsi, color="#4488ff", linewidth=1)
ax2.axhline(70, color="#ff4444", linewidth=0.7, linestyle="--")
ax2.axhline(30, color="#00ff88", linewidth=0.7, linestyle="--")
ax2.set_ylabel("RSI", fontsize=11)
ax2.set_ylim(0, 100)
ax2.tick_params(labelbottom=False)
# MACD panel
colors = ["#00ff88" if v >= 0 else "#ff4444" for v in macd_hist]
ax3.bar(close.index, macd_hist, color=colors, alpha=0.6, width=0.8)
ax3.plot(close.index, macd_line, color="#4488ff", linewidth=1)
ax3.plot(close.index, signal_line, color="#ffaa00", linewidth=1)
ax3.set_ylabel("MACD", fontsize=11)
ax3.set_xlabel("Date", fontsize=11)
fig.savefig(save_path, dpi=150, facecolor="#1a1a2e",
edgecolor="none", bbox_inches="tight")
return fig---
Recipe 4: Correlation Heatmap
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
def correlation_heatmap(returns_df: pd.DataFrame,
save_path: str = "corr.png") -> plt.Figure:
"""Annotated heatmap with diverging red-white-green scale."""
plt.style.use("dark_background")
corr = returns_df.corr()
n = len(corr)
fig, ax = plt.subplots(figsize=(max(8, n * 1.2), max(6, n)))
fig.patch.set_facecolor("#1a1a2e")
ax.set_facecolor("#1a1a2e")
im = ax.imshow(corr.values, cmap="RdYlGn", vmin=-1, vmax=1,
aspect="auto")
ax.set_xticks(range(n))
ax.set_yticks(range(n))
ax.set_xticklabels(corr.columns, rotation=45, ha="right", fontsize=10)
ax.set_yticklabels(corr.columns, fontsize=10)
for i in range(n):
for j in range(n):
val = corr.iloc[i, j]
color = "black" if abs(val) < 0.5 else "white"
ax.text(j, i, f"{val:.2f}", ha="center", va="center",
fontsize=9, color=color)
fig.colorbar(im, ax=ax, shrink=0.8, label="Correlation")
ax.set_title("Asset Correlation Matrix", fontsize=14, fontweight="bold")
fig.tight_layout()
fig.savefig(save_path, dpi=150, facecolor="#1a1a2e",
edgecolor="none", bbox_inches="tight")
return fig---
Recipe 5: Return Distribution with VaR/CVaR
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy import stats
def return_distribution(returns: pd.Series,
save_path: str = "returns_dist.png") -> plt.Figure:
"""Histogram with normal fit, VaR, and CVaR lines."""
plt.style.use("dark_background")
fig, ax = plt.subplots(figsize=(10, 6))
fig.patch.set_facecolor("#1a1a2e")
ax.set_facecolor("#1a1a2e")
ax.hist(returns, bins=50, density=True, alpha=0.7,
color="#4488ff", edgecolor="#222222")
mu, sigma = returns.mean(), returns.std()
x = np.linspace(returns.min(), returns.max(), 200)
ax.plot(x, stats.norm.pdf(x, mu, sigma), color="#ffaa00",
linewidth=2, label=f"Normal (μ={mu:.4f}, σ={sigma:.4f})")
var_95 = float(returns.quantile(0.05))
cvar_95 = float(returns[returns <= var_95].mean())
ax.axvline(var_95, color="#ff4444", linestyle="--", linewidth=1.5,
label=f"VaR 95%: {var_95:.4f}")
ax.axvline(cvar_95, color="#ff6600", linestyle=":", linewidth=1.5,
label=f"CVaR 95%: {cvar_95:.4f}")
ax.set_title("Return Distribution", fontsize=14, fontweight="bold")
ax.set_xlabel("Return", fontsize=11)
ax.set_ylabel("Density", fontsize=11)
ax.legend(fontsize=9)
ax.grid(True, alpha=0.3, color="#333333")
fig.tight_layout()
fig.savefig(save_path, dpi=150, facecolor="#1a1a2e",
edgecolor="none", bbox_inches="tight")
return fig---
Recipe 6: Trade Performance Scatter
import pandas as pd
import matplotlib.pyplot as plt
def trade_scatter(trades: pd.DataFrame,
save_path: str = "trade_scatter.png") -> plt.Figure:
"""Scatter plot: return vs holding period, colored by win/loss."""
plt.style.use("dark_background")
fig, ax = plt.subplots(figsize=(10, 7))
fig.patch.set_facecolor("#1a1a2e")
ax.set_facecolor("#1a1a2e")
wins = trades[trades["return_pct"] >= 0]
losses = trades[trades["return_pct"] < 0]
ax.scatter(wins["hold_hours"], wins["return_pct"],
s=wins["size_usd"] / 10, color="#00ff88", alpha=0.6,
label=f"Wins ({len(wins)})")
ax.scatter(losses["hold_hours"], losses["return_pct"],
s=losses["size_usd"].abs() / 10, color="#ff4444", alpha=0.6,
label=f"Losses ({len(losses)})")
ax.axhline(0, color="#555555", linewidth=0.8)
ax.set_xlabel("Holding Period (hours)", fontsize=11)
ax.set_ylabel("Return %", fontsize=11)
ax.set_title("Trade Performance", fontsize=14, fontweight="bold")
ax.legend(fontsize=10)
ax.grid(True, alpha=0.3, color="#333333")
fig.tight_layout()
fig.savefig(save_path, dpi=150, facecolor="#1a1a2e",
edgecolor="none", bbox_inches="tight")
return figStyling Guide
Complete reference for dark theme trading chart aesthetics: colors, typography, layout ratios, and export settings.
---
Dark Theme Setup
Method 1: Style sheet + rcParams override
import matplotlib.pyplot as plt
plt.style.use("dark_background")
plt.rcParams.update({
"figure.facecolor": "#1a1a2e",
"axes.facecolor": "#1a1a2e",
"axes.edgecolor": "#333333",
"axes.labelcolor": "#e0e0e0",
"axes.titleweight": "bold",
"grid.color": "#333333",
"grid.alpha": 0.4,
"grid.linestyle": "--",
"text.color": "#e0e0e0",
"xtick.color": "#aaaaaa",
"ytick.color": "#aaaaaa",
"legend.facecolor": "#1a1a2e",
"legend.edgecolor": "#333333",
"figure.figsize": [14, 8],
"savefig.facecolor": "#1a1a2e",
"savefig.edgecolor": "none",
"savefig.dpi": 150,
})Method 2: Per-figure styling (no global mutation)
fig, ax = plt.subplots(figsize=(14, 8))
fig.patch.set_facecolor("#1a1a2e")
ax.set_facecolor("#1a1a2e")
ax.spines["bottom"].set_color("#333333")
ax.spines["left"].set_color("#333333")
ax.spines["top"].set_visible(False)
ax.spines["right"].set_visible(False)
ax.tick_params(colors="#aaaaaa")
ax.grid(True, alpha=0.3, color="#333333", linestyle="--")Method 3: mplfinance custom style
import mplfinance as mpf
mc = mpf.make_marketcolors(
up="#00ff88", down="#ff4444",
wick={"up": "#00ff88", "down": "#ff4444"},
edge={"up": "#00ff88", "down": "#ff4444"},
volume={"up": "#00ff88", "down": "#ff4444"},
)
dark_style = mpf.make_mpf_style(
base_mpf_style="nightclouds",
marketcolors=mc,
facecolor="#1a1a2e",
figcolor="#1a1a2e",
gridcolor="#333333",
gridstyle="--",
y_on_right=True,
)---
Color Palette
Primary trading colors
| Role | Name | Hex | RGB |
|---|---|---|---|
| Bullish / Profit | Green | #00ff88 | (0, 255, 136) |
| Bearish / Loss | Red | #ff4444 | (255, 68, 68) |
| Neutral / Info | Blue | #4488ff | (68, 136, 255) |
| Warning / Caution | Amber | #ffaa00 | (255, 170, 0) |
| Background | Dark navy | #1a1a2e | (26, 26, 46) |
| Grid / Border | Dark gray | #333333 | (51, 51, 51) |
| Text primary | Light gray | #e0e0e0 | (224, 224, 224) |
| Text secondary | Mid gray | #aaaaaa | (170, 170, 170) |
| Muted element | Dim gray | #555555 | (85, 85, 85) |
Moving average colors
| Line | Color | Hex |
|---|---|---|
| MA short (10-20) | Orange | #ff6600 |
| MA medium (50) | Blue | #3399ff |
| MA long (100-200) | Yellow | #ffcc00 |
Multi-series palette (for 5+ overlapping lines)
SERIES_COLORS = [
"#00ff88", # green
"#4488ff", # blue
"#ff6600", # orange
"#ffcc00", # yellow
"#cc44ff", # purple
"#ff4488", # pink
"#00cccc", # teal
"#ff8844", # coral
]Alternative background: GitHub dark
Use #0d1117 for a slightly cooler tone matching GitHub's dark mode.
---
Typography
Font sizes
| Element | Size | Weight | Color |
|---|---|---|---|
| Chart title | 14pt | Bold | #e0e0e0 |
| Axis label | 11pt | Normal | #e0e0e0 |
| Tick label | 9pt | Normal | #aaaaaa |
| Legend text | 9-10pt | Normal | #e0e0e0 |
| Annotation | 10pt | Normal | #e0e0e0 |
| Stat box text | 10pt | Monospace | #e0e0e0 |
Applying fonts
ax.set_title("Price Chart", fontsize=14, fontweight="bold", color="#e0e0e0")
ax.set_xlabel("Date", fontsize=11, color="#e0e0e0")
ax.set_ylabel("Price (SOL)", fontsize=11, color="#e0e0e0")
ax.tick_params(axis="both", labelsize=9, colors="#aaaaaa")
ax.legend(fontsize=9, facecolor="#1a1a2e", edgecolor="#333333")Stat box (performance summary on chart)
stats_text = (
f"Return: {total_return:.1%}\n"
f"Sharpe: {sharpe:.2f}\n"
f"Max DD: {max_dd:.1%}"
)
ax.text(0.02, 0.98, stats_text, transform=ax.transAxes,
fontsize=10, fontfamily="monospace", color="#e0e0e0",
verticalalignment="top",
bbox=dict(boxstyle="round,pad=0.5", facecolor="#222233",
edgecolor="#333333", alpha=0.9))---
Layout Principles
Figure sizes
| Chart Type | Size (inches) | Aspect |
|---|---|---|
| Full analysis (multi-panel) | (14, 10) | Wide |
| Standard chart | (14, 8) | Wide |
| Simple plot | (10, 6) | Standard |
| Heatmap (square) | (10, 8) | Near-square |
| Dashboard tile | (7, 5) | Compact |
Multi-panel height ratios
| Layout | Ratios | Use |
|---|---|---|
| Price + Volume | [3, 1] | Basic OHLCV |
| Equity + Drawdown | [2, 1] | Performance |
| Price + Vol + Indicator | [3, 1, 1] | Full analysis |
| Price + RSI + MACD | [3, 1, 1] | Indicator stack |
| 4-panel | [3, 1, 1, 1] | Deep analysis |
Spacing
# Option 1: tight_layout (simple)
fig.tight_layout()
# Option 2: constrained_layout (better for complex)
fig, axes = plt.subplots(3, 1, figsize=(14, 10),
constrained_layout=True)
# Option 3: GridSpec with hspace
gs = gridspec.GridSpec(3, 1, height_ratios=[3, 1, 1], hspace=0.05)Use hspace=0.05 for shared-axis panels (price + volume) to make them look connected. Use hspace=0.15 or tight_layout() when panels are independent.
---
Saving Charts
Standard save
fig.savefig("chart.png", dpi=150,
facecolor=fig.get_facecolor(),
edgecolor="none",
bbox_inches="tight")DPI guidelines
| Use | DPI |
|---|---|
| Screen / sharing | 150 |
| Presentation | 200 |
| Publication / print | 300 |
Format comparison
| Format | Size | Quality | Editable | Use |
|---|---|---|---|---|
| PNG | Medium | Raster | No | Default for sharing |
| SVG | Small | Vector | Yes | Editing, scaling |
| Medium | Vector | Partial | Reports, papers | |
| HTML | Large | Interactive | N/A | Plotly dashboards |
Transparent background (for embedding)
fig.savefig("chart.png", dpi=150, transparent=True,
bbox_inches="tight")Plotly dark template
import plotly.io as pio
pio.templates["trading_dark"] = go.layout.Template(
layout=dict(
paper_bgcolor="#1a1a2e",
plot_bgcolor="#1a1a2e",
font=dict(color="#e0e0e0"),
xaxis=dict(gridcolor="#333333"),
yaxis=dict(gridcolor="#333333"),
)
)
pio.templates.default = "trading_dark"#!/usr/bin/env python3
"""Generate professional trading charts from synthetic data.
Creates four chart types commonly used in trading analysis:
1. Candlestick chart with EMA overlays and volume bars
2. Equity curve with drawdown panel
3. Return distribution histogram with normal fit
4. Price chart with entry/exit trade markers
All charts use dark theme styling suitable for trading dashboards.
Outputs are saved as PNG files in the current directory.
Usage:
python scripts/chart_generator.py
python scripts/chart_generator.py --output-dir ./charts
Dependencies:
uv pip install matplotlib mplfinance pandas numpy scipy
Environment Variables:
None required — uses synthetic data for demonstration.
"""
import argparse
import os
import sys
from pathlib import Path
from typing import Optional
import matplotlib
matplotlib.use("Agg") # Non-interactive backend for file output
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
import numpy as np
import pandas as pd
from scipy import stats
# ── Configuration ───────────────────────────────────────────────────
BACKGROUND = "#1a1a2e"
GRID_COLOR = "#333333"
GREEN = "#00ff88"
RED = "#ff4444"
BLUE = "#4488ff"
AMBER = "#ffaa00"
ORANGE = "#ff6600"
LIGHT_BLUE = "#3399ff"
TEXT_COLOR = "#e0e0e0"
TEXT_SECONDARY = "#aaaaaa"
DIM = "#555555"
FIGURE_DPI = 150
BARS = 100
SEED = 42
# ── Dark Theme Setup ───────────────────────────────────────────────
def apply_dark_theme() -> None:
"""Apply dark background trading theme to matplotlib."""
plt.style.use("dark_background")
plt.rcParams.update({
"figure.facecolor": BACKGROUND,
"axes.facecolor": BACKGROUND,
"axes.edgecolor": GRID_COLOR,
"axes.labelcolor": TEXT_COLOR,
"grid.color": GRID_COLOR,
"grid.alpha": 0.4,
"grid.linestyle": "--",
"text.color": TEXT_COLOR,
"xtick.color": TEXT_SECONDARY,
"ytick.color": TEXT_SECONDARY,
"legend.facecolor": BACKGROUND,
"legend.edgecolor": GRID_COLOR,
"savefig.facecolor": BACKGROUND,
"savefig.edgecolor": "none",
"savefig.dpi": FIGURE_DPI,
})
# ── Synthetic Data Generation ──────────────────────────────────────
def generate_ohlcv(n_bars: int = BARS, seed: int = SEED) -> pd.DataFrame:
"""Generate synthetic OHLCV data resembling a volatile token.
Args:
n_bars: Number of candlestick bars to generate.
seed: Random seed for reproducibility.
Returns:
DataFrame with DatetimeIndex and Open/High/Low/Close/Volume columns.
"""
rng = np.random.default_rng(seed)
dates = pd.date_range("2025-01-01", periods=n_bars, freq="1h")
# Random walk with drift for close prices
log_returns = rng.normal(0.0005, 0.02, n_bars)
close = 100.0 * np.exp(np.cumsum(log_returns))
# Derive OHLV from close
spread = close * rng.uniform(0.005, 0.025, n_bars)
high = close + spread * rng.uniform(0.3, 1.0, n_bars)
low = close - spread * rng.uniform(0.3, 1.0, n_bars)
open_price = close + rng.normal(0, spread * 0.3)
# Ensure OHLC consistency
high = np.maximum(high, np.maximum(open_price, close))
low = np.minimum(low, np.minimum(open_price, close))
volume = rng.lognormal(mean=10, sigma=0.8, size=n_bars)
df = pd.DataFrame({
"Open": open_price,
"High": high,
"Low": low,
"Close": close,
"Volume": volume,
}, index=dates)
return df
def generate_equity_curve(n_days: int = 252, seed: int = SEED) -> pd.Series:
"""Generate a synthetic equity curve over n_days.
Args:
n_days: Number of trading days.
seed: Random seed for reproducibility.
Returns:
Series indexed by date with portfolio value.
"""
rng = np.random.default_rng(seed)
dates = pd.date_range("2025-01-01", periods=n_days, freq="B")
daily_returns = rng.normal(0.0008, 0.015, n_days)
# Add a drawdown period in the middle
drawdown_start = n_days // 3
drawdown_end = drawdown_start + n_days // 6
daily_returns[drawdown_start:drawdown_end] -= 0.008
equity = 10_000.0 * np.cumprod(1 + daily_returns)
return pd.Series(equity, index=dates, name="Equity")
def generate_trades(price: pd.Series, n_trades: int = 15,
seed: int = SEED) -> pd.DataFrame:
"""Generate synthetic trade entries and exits on a price series.
Args:
price: Price series with DatetimeIndex.
n_trades: Number of trades to generate.
seed: Random seed for reproducibility.
Returns:
DataFrame with columns: entry_date, entry_price, exit_date,
exit_price, return_pct, side.
"""
rng = np.random.default_rng(seed)
n = len(price)
trades = []
for _ in range(n_trades):
entry_idx = rng.integers(5, n - 10)
hold = rng.integers(2, 8)
exit_idx = min(entry_idx + hold, n - 1)
entry_price = float(price.iloc[entry_idx])
exit_price = float(price.iloc[exit_idx])
side = "long" if rng.random() > 0.3 else "short"
if side == "long":
ret = (exit_price - entry_price) / entry_price
else:
ret = (entry_price - exit_price) / entry_price
trades.append({
"entry_date": price.index[entry_idx],
"entry_price": entry_price,
"exit_date": price.index[exit_idx],
"exit_price": exit_price,
"return_pct": ret,
"side": side,
})
return pd.DataFrame(trades)
# ── Chart 1: Candlestick with EMAs ─────────────────────────────────
def chart_candlestick(df: pd.DataFrame, output_dir: str) -> str:
"""Create candlestick chart with EMA overlays and volume.
Args:
df: OHLCV DataFrame with DatetimeIndex.
output_dir: Directory to save the chart.
Returns:
Path to saved chart file.
"""
try:
import mplfinance as mpf
except ImportError:
print("mplfinance not installed. Run: uv pip install mplfinance")
sys.exit(1)
ema20 = df["Close"].ewm(span=20).mean()
ema50 = df["Close"].ewm(span=50).mean()
ap = [
mpf.make_addplot(ema20, color=ORANGE, width=1.2),
mpf.make_addplot(ema50, color=LIGHT_BLUE, width=1.2),
]
mc = mpf.make_marketcolors(
up=GREEN, down=RED,
wick={"up": GREEN, "down": RED},
edge={"up": GREEN, "down": RED},
volume={"up": GREEN, "down": RED},
)
style = mpf.make_mpf_style(
base_mpf_style="nightclouds", marketcolors=mc,
facecolor=BACKGROUND, figcolor=BACKGROUND,
gridcolor=GRID_COLOR, gridstyle="--",
)
save_path = os.path.join(output_dir, "candlestick.png")
mpf.plot(
df, type="candle", style=style, addplot=ap,
volume=True, figsize=(14, 8),
title="\nCandlestick — EMA(20, 50) with Volume",
savefig=dict(fname=save_path, dpi=FIGURE_DPI, facecolor=BACKGROUND),
)
return save_path
# ── Chart 2: Equity Curve with Drawdown ─────────────────────────────
def chart_equity_drawdown(equity: pd.Series, output_dir: str) -> str:
"""Create equity curve with drawdown panel.
Args:
equity: Portfolio equity series.
output_dir: Directory to save the chart.
Returns:
Path to saved chart file.
"""
peak = equity.cummax()
drawdown = (equity - peak) / peak
fig, (ax1, ax2) = plt.subplots(
2, 1, figsize=(14, 8), height_ratios=[2, 1], sharex=True
)
# Equity panel
ax1.plot(equity.index, equity, color=GREEN, linewidth=1.5, label="Equity")
ax1.plot(equity.index, peak, color=DIM, linewidth=0.8, linestyle="--",
label="Peak")
ax1.set_ylabel("Portfolio Value ($)", fontsize=11)
ax1.set_title("Equity Curve with Drawdown", fontsize=14, fontweight="bold")
ax1.legend(loc="upper left", fontsize=9)
ax1.grid(True, alpha=0.3)
# Stats box
total_ret = (equity.iloc[-1] / equity.iloc[0]) - 1
max_dd = float(drawdown.min())
stats_text = f"Return: {total_ret:.1%}\nMax DD: {max_dd:.1%}"
ax1.text(
0.02, 0.95, stats_text, transform=ax1.transAxes,
fontsize=10, fontfamily="monospace", verticalalignment="top",
bbox=dict(boxstyle="round,pad=0.4", facecolor="#222233",
edgecolor=GRID_COLOR, alpha=0.9),
)
# Drawdown panel
ax2.fill_between(equity.index, drawdown, 0, color=RED, alpha=0.5)
ax2.plot(equity.index, drawdown, color=RED, linewidth=0.8)
ax2.set_ylabel("Drawdown", fontsize=11)
ax2.set_xlabel("Date", fontsize=11)
ax2.grid(True, alpha=0.3)
fig.tight_layout()
save_path = os.path.join(output_dir, "equity_drawdown.png")
fig.savefig(save_path, dpi=FIGURE_DPI, facecolor=BACKGROUND,
edgecolor="none", bbox_inches="tight")
plt.close(fig)
return save_path
# ── Chart 3: Return Distribution ────────────────────────────────────
def chart_return_distribution(returns: pd.Series, output_dir: str) -> str:
"""Create return distribution histogram with normal fit and VaR.
Args:
returns: Series of periodic returns.
output_dir: Directory to save the chart.
Returns:
Path to saved chart file.
"""
fig, ax = plt.subplots(figsize=(10, 6))
ax.hist(returns, bins=50, density=True, alpha=0.7,
color=BLUE, edgecolor="#222222")
# Normal distribution overlay
mu = float(returns.mean())
sigma = float(returns.std())
x = np.linspace(float(returns.min()), float(returns.max()), 200)
ax.plot(x, stats.norm.pdf(x, mu, sigma), color=AMBER,
linewidth=2, label=f"Normal (mu={mu:.4f}, sigma={sigma:.4f})")
# VaR and CVaR lines
var_95 = float(returns.quantile(0.05))
tail = returns[returns <= var_95]
cvar_95 = float(tail.mean()) if len(tail) > 0 else var_95
ax.axvline(var_95, color=RED, linestyle="--", linewidth=1.5,
label=f"VaR 95%: {var_95:.4f}")
ax.axvline(cvar_95, color=ORANGE, linestyle=":", linewidth=1.5,
label=f"CVaR 95%: {cvar_95:.4f}")
ax.set_title("Return Distribution", fontsize=14, fontweight="bold")
ax.set_xlabel("Daily Return", fontsize=11)
ax.set_ylabel("Density", fontsize=11)
ax.legend(fontsize=9)
ax.grid(True, alpha=0.3)
fig.tight_layout()
save_path = os.path.join(output_dir, "return_distribution.png")
fig.savefig(save_path, dpi=FIGURE_DPI, facecolor=BACKGROUND,
edgecolor="none", bbox_inches="tight")
plt.close(fig)
return save_path
# ── Chart 4: Trade Markers on Price ──────────────────────────────────
def chart_trade_markers(price: pd.Series, trades: pd.DataFrame,
output_dir: str) -> str:
"""Create price chart with entry/exit trade markers.
Args:
price: Close price series.
trades: DataFrame with entry_date, entry_price, exit_date,
exit_price, return_pct, side columns.
output_dir: Directory to save the chart.
Returns:
Path to saved chart file.
"""
fig, ax = plt.subplots(figsize=(14, 7))
# Price line
ax.plot(price.index, price, color="#cccccc", linewidth=1, alpha=0.8,
label="Price")
# Entry markers
for _, t in trades.iterrows():
marker = "^" if t["side"] == "long" else "v"
color = GREEN if t["side"] == "long" else RED
ax.scatter(t["entry_date"], t["entry_price"],
marker=marker, color=color, s=100, zorder=5, edgecolors="white",
linewidths=0.5)
# Exit markers (colored by P&L)
for _, t in trades.iterrows():
exit_color = GREEN if t["return_pct"] >= 0 else RED
ax.scatter(t["exit_date"], t["exit_price"],
marker="x", color=exit_color, s=80, zorder=5, linewidths=2)
# Connect entry to exit
for _, t in trades.iterrows():
line_color = GREEN if t["return_pct"] >= 0 else RED
ax.plot([t["entry_date"], t["exit_date"]],
[t["entry_price"], t["exit_price"]],
color=line_color, linewidth=0.6, alpha=0.4)
# Summary stats
wins = len(trades[trades["return_pct"] >= 0])
total = len(trades)
win_rate = wins / total if total > 0 else 0
avg_ret = float(trades["return_pct"].mean())
stats_text = (
f"Trades: {total}\n"
f"Win rate: {win_rate:.0%}\n"
f"Avg return: {avg_ret:.2%}"
)
ax.text(
0.02, 0.95, stats_text, transform=ax.transAxes,
fontsize=10, fontfamily="monospace", verticalalignment="top",
bbox=dict(boxstyle="round,pad=0.4", facecolor="#222233",
edgecolor=GRID_COLOR, alpha=0.9),
)
# Legend entries for marker types
ax.scatter([], [], marker="^", color=GREEN, s=60, label="Long entry")
ax.scatter([], [], marker="v", color=RED, s=60, label="Short entry")
ax.scatter([], [], marker="x", color=GREEN, s=60, label="Win exit")
ax.scatter([], [], marker="x", color=RED, s=60, label="Loss exit")
ax.set_title("Trade Markers on Price", fontsize=14, fontweight="bold")
ax.set_xlabel("Date", fontsize=11)
ax.set_ylabel("Price", fontsize=11)
ax.legend(loc="upper right", fontsize=9)
ax.grid(True, alpha=0.3)
fig.tight_layout()
save_path = os.path.join(output_dir, "trade_markers.png")
fig.savefig(save_path, dpi=FIGURE_DPI, facecolor=BACKGROUND,
edgecolor="none", bbox_inches="tight")
plt.close(fig)
return save_path
# ── Main ────────────────────────────────────────────────────────────
def parse_args() -> argparse.Namespace:
"""Parse command-line arguments."""
parser = argparse.ArgumentParser(
description="Generate professional trading charts from synthetic data."
)
parser.add_argument(
"--output-dir", type=str, default=".",
help="Directory to save chart PNGs (default: current directory)."
)
return parser.parse_args()
def main() -> None:
"""Generate all demo charts and print file paths."""
args = parse_args()
output_dir = args.output_dir
os.makedirs(output_dir, exist_ok=True)
apply_dark_theme()
print("Generating synthetic trading data...")
# Generate data
ohlcv = generate_ohlcv()
equity = generate_equity_curve()
daily_returns = equity.pct_change().dropna()
trades = generate_trades(ohlcv["Close"])
print(f" OHLCV: {len(ohlcv)} bars")
print(f" Equity: {len(equity)} days")
print(f" Trades: {len(trades)} trades")
print()
# Generate charts
charts = []
print("Creating candlestick chart...")
charts.append(chart_candlestick(ohlcv, output_dir))
print("Creating equity curve with drawdown...")
charts.append(chart_equity_drawdown(equity, output_dir))
print("Creating return distribution...")
charts.append(chart_return_distribution(daily_returns, output_dir))
print("Creating trade markers chart...")
charts.append(chart_trade_markers(ohlcv["Close"], trades, output_dir))
print()
print("Charts generated:")
for path in charts:
abs_path = os.path.abspath(path)
print(f" {abs_path}")
print()
print(f"All {len(charts)} charts saved to: {os.path.abspath(output_dir)}")
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Generate a multi-chart performance report from a portfolio equity curve.
Creates three report pages as individual PNG files:
Page 1: Equity curve + drawdown panel
Page 2: Monthly returns table + return distribution histogram
Page 3: Trade analysis — win/loss bar chart + holding period distribution
Also prints a summary of key performance statistics to the console.
Uses synthetic demo data by default.
Usage:
python scripts/performance_report.py
python scripts/performance_report.py --output-dir ./reports
python scripts/performance_report.py --days 500
Dependencies:
uv pip install matplotlib pandas numpy scipy
Environment Variables:
None required — uses synthetic data for demonstration.
"""
import argparse
import os
import sys
from typing import Optional
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from scipy import stats
# ── Configuration ───────────────────────────────────────────────────
BACKGROUND = "#1a1a2e"
GRID_COLOR = "#333333"
GREEN = "#00ff88"
RED = "#ff4444"
BLUE = "#4488ff"
AMBER = "#ffaa00"
ORANGE = "#ff6600"
TEXT_COLOR = "#e0e0e0"
DIM = "#555555"
FIGURE_DPI = 150
SEED = 42
# ── Dark Theme ──────────────────────────────────────────────────────
def apply_dark_theme() -> None:
"""Apply dark background trading theme globally."""
plt.style.use("dark_background")
plt.rcParams.update({
"figure.facecolor": BACKGROUND,
"axes.facecolor": BACKGROUND,
"axes.edgecolor": GRID_COLOR,
"axes.labelcolor": TEXT_COLOR,
"grid.color": GRID_COLOR,
"grid.alpha": 0.4,
"grid.linestyle": "--",
"text.color": TEXT_COLOR,
"xtick.color": "#aaaaaa",
"ytick.color": "#aaaaaa",
"legend.facecolor": BACKGROUND,
"legend.edgecolor": GRID_COLOR,
"savefig.facecolor": BACKGROUND,
"savefig.edgecolor": "none",
"savefig.dpi": FIGURE_DPI,
})
# ── Data Generation ────────────────────────────────────────────────
def generate_equity(n_days: int = 365, initial: float = 10_000.0,
seed: int = SEED) -> pd.Series:
"""Generate synthetic equity curve with realistic characteristics.
Args:
n_days: Number of trading days.
initial: Starting portfolio value.
seed: Random seed.
Returns:
Series of portfolio values indexed by business day.
"""
rng = np.random.default_rng(seed)
dates = pd.date_range("2025-01-01", periods=n_days, freq="B")
# Base returns with slight positive drift
daily_ret = rng.normal(0.0006, 0.014, n_days)
# Inject a drawdown period
dd_start = n_days // 4
dd_end = dd_start + n_days // 8
daily_ret[dd_start:dd_end] -= 0.007
# Inject a rally
rally_start = n_days * 2 // 3
rally_end = rally_start + n_days // 10
daily_ret[rally_start:rally_end] += 0.005
equity = initial * np.cumprod(1 + daily_ret)
return pd.Series(equity, index=dates, name="Equity")
def generate_trade_list(n_trades: int = 80, seed: int = SEED) -> pd.DataFrame:
"""Generate a synthetic list of closed trades.
Args:
n_trades: Number of trades.
seed: Random seed.
Returns:
DataFrame with columns: entry_date, exit_date, return_pct,
hold_hours, size_usd, side.
"""
rng = np.random.default_rng(seed)
base = pd.Timestamp("2025-01-01")
trades = []
for i in range(n_trades):
entry_date = base + pd.Timedelta(hours=int(rng.integers(0, 8000)))
hold_hours = int(rng.integers(1, 72))
exit_date = entry_date + pd.Timedelta(hours=hold_hours)
# 55% win rate with positive skew
is_win = rng.random() < 0.55
if is_win:
ret = float(rng.exponential(0.04))
else:
ret = -float(rng.exponential(0.035))
size_usd = float(rng.uniform(200, 2000))
side = "long" if rng.random() > 0.25 else "short"
trades.append({
"entry_date": entry_date,
"exit_date": exit_date,
"return_pct": ret,
"hold_hours": hold_hours,
"size_usd": size_usd,
"side": side,
})
return pd.DataFrame(trades)
# ── Performance Metrics ─────────────────────────────────────────────
def compute_metrics(equity: pd.Series, trades: pd.DataFrame) -> dict:
"""Compute key performance metrics.
Args:
equity: Portfolio equity series.
trades: Trade list DataFrame.
Returns:
Dict of metric name to value.
"""
returns = equity.pct_change().dropna()
peak = equity.cummax()
drawdown = (equity - peak) / peak
total_return = (equity.iloc[-1] / equity.iloc[0]) - 1
ann_return = (1 + total_return) ** (252 / len(equity)) - 1
ann_vol = float(returns.std()) * np.sqrt(252)
sharpe = ann_return / ann_vol if ann_vol > 0 else 0.0
max_dd = float(drawdown.min())
# Trade metrics
wins = trades[trades["return_pct"] > 0]
losses = trades[trades["return_pct"] <= 0]
win_rate = len(wins) / len(trades) if len(trades) > 0 else 0
avg_win = float(wins["return_pct"].mean()) if len(wins) > 0 else 0
avg_loss = float(losses["return_pct"].mean()) if len(losses) > 0 else 0
profit_factor = (
abs(float(wins["return_pct"].sum()) / float(losses["return_pct"].sum()))
if len(losses) > 0 and float(losses["return_pct"].sum()) != 0
else float("inf")
)
avg_hold = float(trades["hold_hours"].mean())
return {
"total_return": total_return,
"ann_return": ann_return,
"ann_volatility": ann_vol,
"sharpe_ratio": sharpe,
"max_drawdown": max_dd,
"total_trades": len(trades),
"win_rate": win_rate,
"avg_win": avg_win,
"avg_loss": avg_loss,
"profit_factor": profit_factor,
"avg_hold_hours": avg_hold,
}
def print_metrics(metrics: dict) -> None:
"""Print performance metrics to console."""
print("=" * 50)
print(" PERFORMANCE SUMMARY")
print("=" * 50)
print(f" Total Return: {metrics['total_return']:>10.2%}")
print(f" Ann. Return: {metrics['ann_return']:>10.2%}")
print(f" Ann. Volatility: {metrics['ann_volatility']:>10.2%}")
print(f" Sharpe Ratio: {metrics['sharpe_ratio']:>10.2f}")
print(f" Max Drawdown: {metrics['max_drawdown']:>10.2%}")
print("-" * 50)
print(f" Total Trades: {metrics['total_trades']:>10d}")
print(f" Win Rate: {metrics['win_rate']:>10.1%}")
print(f" Avg Win: {metrics['avg_win']:>10.2%}")
print(f" Avg Loss: {metrics['avg_loss']:>10.2%}")
print(f" Profit Factor: {metrics['profit_factor']:>10.2f}")
print(f" Avg Hold (hrs): {metrics['avg_hold_hours']:>10.1f}")
print("=" * 50)
# ── Page 1: Equity Curve + Drawdown ─────────────────────────────────
def page_equity(equity: pd.Series, metrics: dict,
output_dir: str) -> str:
"""Generate equity curve with drawdown panel.
Args:
equity: Portfolio equity series.
metrics: Pre-computed performance metrics.
output_dir: Directory to save output.
Returns:
Path to saved chart.
"""
peak = equity.cummax()
dd = (equity - peak) / peak
fig, (ax1, ax2) = plt.subplots(
2, 1, figsize=(14, 8), height_ratios=[2, 1], sharex=True
)
ax1.plot(equity.index, equity, color=GREEN, linewidth=1.5, label="Equity")
ax1.plot(equity.index, peak, color=DIM, linewidth=0.8, linestyle="--",
label="Peak")
ax1.set_ylabel("Portfolio Value ($)", fontsize=11)
ax1.set_title("Performance Report — Equity Curve", fontsize=14,
fontweight="bold")
ax1.legend(loc="upper left", fontsize=9)
ax1.grid(True, alpha=0.3)
# Stats box
stats_text = (
f"Return: {metrics['total_return']:.1%}\n"
f"Sharpe: {metrics['sharpe_ratio']:.2f}\n"
f"Max DD: {metrics['max_drawdown']:.1%}\n"
f"Win Rate: {metrics['win_rate']:.0%}"
)
ax1.text(
0.02, 0.95, stats_text, transform=ax1.transAxes,
fontsize=10, fontfamily="monospace", verticalalignment="top",
bbox=dict(boxstyle="round,pad=0.4", facecolor="#222233",
edgecolor=GRID_COLOR, alpha=0.9),
)
ax2.fill_between(equity.index, dd, 0, color=RED, alpha=0.5)
ax2.plot(equity.index, dd, color=RED, linewidth=0.8)
ax2.set_ylabel("Drawdown", fontsize=11)
ax2.set_xlabel("Date", fontsize=11)
ax2.grid(True, alpha=0.3)
fig.tight_layout()
save_path = os.path.join(output_dir, "report_p1_equity.png")
fig.savefig(save_path, dpi=FIGURE_DPI, facecolor=BACKGROUND,
edgecolor="none", bbox_inches="tight")
plt.close(fig)
return save_path
# ── Page 2: Monthly Returns + Distribution ──────────────────────────
def page_returns(equity: pd.Series, output_dir: str) -> str:
"""Generate monthly returns text table and return distribution.
Args:
equity: Portfolio equity series.
output_dir: Directory to save output.
Returns:
Path to saved chart.
"""
returns = equity.pct_change().dropna()
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 7))
# Left: Monthly returns as text table
monthly = returns.resample("ME").apply(lambda x: (1 + x).prod() - 1)
monthly_df = pd.DataFrame({
"Month": monthly.index.strftime("%Y-%m"),
"Return": monthly.values,
})
ax1.axis("off")
ax1.set_title("Monthly Returns", fontsize=14, fontweight="bold",
pad=20)
# Build table data
table_data = []
cell_colors = []
for _, row in monthly_df.iterrows():
ret_val = row["Return"]
table_data.append([row["Month"], f"{ret_val:.2%}"])
if ret_val >= 0:
cell_colors.append([BACKGROUND, "#1a3a2e"])
else:
cell_colors.append([BACKGROUND, "#3a1a1e"])
if table_data:
table = ax1.table(
cellText=table_data,
colLabels=["Month", "Return"],
cellColours=cell_colors,
colColours=[GRID_COLOR, GRID_COLOR],
loc="center",
cellLoc="center",
)
table.auto_set_font_size(False)
table.set_fontsize(9)
table.scale(0.8, 1.4)
for key, cell in table.get_celld().items():
cell.set_edgecolor(GRID_COLOR)
cell.set_text_props(color=TEXT_COLOR)
# Right: Return distribution
ax2.hist(returns, bins=50, density=True, alpha=0.7,
color=BLUE, edgecolor="#222222")
mu = float(returns.mean())
sigma = float(returns.std())
x = np.linspace(float(returns.min()), float(returns.max()), 200)
ax2.plot(x, stats.norm.pdf(x, mu, sigma), color=AMBER, linewidth=2,
label=f"Normal (mu={mu:.4f})")
var_95 = float(returns.quantile(0.05))
ax2.axvline(var_95, color=RED, linestyle="--", linewidth=1.5,
label=f"VaR 95%: {var_95:.4f}")
ax2.set_title("Return Distribution", fontsize=14, fontweight="bold")
ax2.set_xlabel("Daily Return", fontsize=11)
ax2.set_ylabel("Density", fontsize=11)
ax2.legend(fontsize=9)
ax2.grid(True, alpha=0.3)
fig.tight_layout()
save_path = os.path.join(output_dir, "report_p2_returns.png")
fig.savefig(save_path, dpi=FIGURE_DPI, facecolor=BACKGROUND,
edgecolor="none", bbox_inches="tight")
plt.close(fig)
return save_path
# ── Page 3: Trade Analysis ──────────────────────────────────────────
def page_trades(trades: pd.DataFrame, output_dir: str) -> str:
"""Generate trade analysis: win/loss distribution + holding period.
Args:
trades: Trade list DataFrame.
output_dir: Directory to save output.
Returns:
Path to saved chart.
"""
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 7))
# Left: Return distribution by trade
wins = trades[trades["return_pct"] >= 0]["return_pct"]
losses = trades[trades["return_pct"] < 0]["return_pct"]
bins = np.linspace(
float(trades["return_pct"].min()) - 0.01,
float(trades["return_pct"].max()) + 0.01,
30
)
ax1.hist(wins, bins=bins, alpha=0.7, color=GREEN, label=f"Wins ({len(wins)})",
edgecolor="#222222")
ax1.hist(losses, bins=bins, alpha=0.7, color=RED,
label=f"Losses ({len(losses)})", edgecolor="#222222")
ax1.axvline(0, color=DIM, linewidth=1, linestyle="-")
ax1.set_title("Trade Returns Distribution", fontsize=14,
fontweight="bold")
ax1.set_xlabel("Return per Trade", fontsize=11)
ax1.set_ylabel("Count", fontsize=11)
ax1.legend(fontsize=10)
ax1.grid(True, alpha=0.3)
# Right: Holding period distribution
hold_bins = np.arange(0, float(trades["hold_hours"].max()) + 5, 4)
colors_hold = []
for i in range(len(hold_bins) - 1):
mask = (trades["hold_hours"] >= hold_bins[i]) & (
trades["hold_hours"] < hold_bins[i + 1]
)
subset = trades.loc[mask, "return_pct"]
avg_ret = float(subset.mean()) if len(subset) > 0 else 0
colors_hold.append(GREEN if avg_ret >= 0 else RED)
counts, _, patches = ax2.hist(
trades["hold_hours"], bins=hold_bins, alpha=0.7,
color=BLUE, edgecolor="#222222"
)
for patch, c in zip(patches, colors_hold):
patch.set_facecolor(c)
patch.set_alpha(0.7)
ax2.set_title("Holding Period Distribution", fontsize=14,
fontweight="bold")
ax2.set_xlabel("Holding Period (hours)", fontsize=11)
ax2.set_ylabel("Count", fontsize=11)
ax2.grid(True, alpha=0.3)
# Stats
win_rate = len(wins) / len(trades) if len(trades) > 0 else 0
avg_ret = float(trades["return_pct"].mean())
stats_text = (
f"Win rate: {win_rate:.0%} | "
f"Avg return: {avg_ret:.2%} | "
f"Avg hold: {trades['hold_hours'].mean():.0f}h"
)
fig.suptitle(stats_text, fontsize=11, color=TEXT_COLOR, y=0.02,
fontfamily="monospace")
fig.tight_layout(rect=[0, 0.04, 1, 1])
save_path = os.path.join(output_dir, "report_p3_trades.png")
fig.savefig(save_path, dpi=FIGURE_DPI, facecolor=BACKGROUND,
edgecolor="none", bbox_inches="tight")
plt.close(fig)
return save_path
# ── Main ────────────────────────────────────────────────────────────
def parse_args() -> argparse.Namespace:
"""Parse command-line arguments."""
parser = argparse.ArgumentParser(
description="Generate multi-chart performance report."
)
parser.add_argument(
"--output-dir", type=str, default=".",
help="Directory to save report PNGs (default: current directory)."
)
parser.add_argument(
"--days", type=int, default=365,
help="Number of trading days for synthetic equity (default: 365)."
)
return parser.parse_args()
def main() -> None:
"""Generate the full performance report."""
args = parse_args()
output_dir = args.output_dir
os.makedirs(output_dir, exist_ok=True)
apply_dark_theme()
print("Generating synthetic portfolio data...")
equity = generate_equity(n_days=args.days)
trades = generate_trade_list(n_trades=80)
metrics = compute_metrics(equity, trades)
print()
print_metrics(metrics)
print()
pages = []
print("Generating Page 1: Equity Curve + Drawdown...")
pages.append(page_equity(equity, metrics, output_dir))
print("Generating Page 2: Monthly Returns + Distribution...")
pages.append(page_returns(equity, output_dir))
print("Generating Page 3: Trade Analysis...")
pages.append(page_trades(trades, output_dir))
print()
print("Report pages generated:")
for path in pages:
print(f" {os.path.abspath(path)}")
print()
print(f"All {len(pages)} report pages saved to: {os.path.abspath(output_dir)}")
if __name__ == "__main__":
main()
Related skills
FAQ
Which libraries does it use?
mplfinance for candlesticks, matplotlib for general charts, and plotly for interactive HTML charts.
What chart types are covered?
Candlesticks, equity curves, drawdowns, return distributions, correlation heatmaps, trade markers, and indicator panels.