Initial commit: multi-symbol bot with backtest engine and RSI trend strategy
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
"""
|
||||
Equity curve and trade distribution charts.
|
||||
Saved to reports/ as PNG files.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import matplotlib
|
||||
matplotlib.use("Agg") # non-interactive backend
|
||||
import matplotlib.pyplot as plt
|
||||
import matplotlib.gridspec as gridspec
|
||||
|
||||
from engine.backtest import Trade
|
||||
from engine.metrics import Metrics
|
||||
|
||||
REPORTS_DIR = Path(__file__).parent
|
||||
|
||||
|
||||
def save_equity_chart(
|
||||
equity: np.ndarray,
|
||||
trades: list[Trade],
|
||||
metrics: Metrics,
|
||||
label: str,
|
||||
filename: str = "equity.png",
|
||||
) -> Path:
|
||||
closed = [t for t in trades if t.closed]
|
||||
|
||||
# drawdown
|
||||
peak = np.maximum.accumulate(equity)
|
||||
dd = (equity - peak) / peak * 100
|
||||
|
||||
# daily pnl histogram
|
||||
by_day: dict[str, float] = {}
|
||||
for t in closed:
|
||||
day = t.exit_time.date().isoformat() # type: ignore[union-attr]
|
||||
by_day[day] = by_day.get(day, 0) + t.pnl_r
|
||||
daily_r = np.array(list(by_day.values()))
|
||||
|
||||
fig = plt.figure(figsize=(14, 10))
|
||||
fig.suptitle(label, fontsize=13, fontweight="bold")
|
||||
gs = gridspec.GridSpec(3, 2, figure=fig, hspace=0.45, wspace=0.3)
|
||||
|
||||
# ── equity curve ──────────────────────────────────────────────────────────
|
||||
ax1 = fig.add_subplot(gs[0, :])
|
||||
ax1.plot(equity, color="#2196F3", linewidth=1.2, label="Equity")
|
||||
ax1.axhline(equity[0], color="grey", linewidth=0.6, linestyle="--")
|
||||
ax1.set_ylabel("Balance ($)")
|
||||
ax1.set_title("Equity Curve")
|
||||
ax1.legend(fontsize=8)
|
||||
ax1.grid(True, alpha=0.3)
|
||||
|
||||
# ── drawdown ──────────────────────────────────────────────────────────────
|
||||
ax2 = fig.add_subplot(gs[1, :])
|
||||
ax2.fill_between(range(len(dd)), dd, 0, color="#F44336", alpha=0.6)
|
||||
ax2.set_ylabel("Drawdown (%)")
|
||||
ax2.set_title(f"Drawdown (max {metrics.max_drawdown*100:.1f}%)")
|
||||
ax2.grid(True, alpha=0.3)
|
||||
|
||||
# ── R distribution ────────────────────────────────────────────────────────
|
||||
ax3 = fig.add_subplot(gs[2, 0])
|
||||
r_vals = [t.pnl_r for t in closed]
|
||||
colors = ["#4CAF50" if r > 0 else "#F44336" for r in r_vals]
|
||||
ax3.bar(range(len(r_vals)), r_vals, color=colors, width=0.8, alpha=0.8)
|
||||
ax3.axhline(0, color="black", linewidth=0.6)
|
||||
ax3.set_xlabel("Trade #")
|
||||
ax3.set_ylabel("R")
|
||||
ax3.set_title("R per Trade")
|
||||
ax3.grid(True, alpha=0.3, axis="y")
|
||||
|
||||
# ── daily R histogram ─────────────────────────────────────────────────────
|
||||
ax4 = fig.add_subplot(gs[2, 1])
|
||||
if len(daily_r) > 0:
|
||||
ax4.hist(daily_r, bins=30, color="#9C27B0", alpha=0.75, edgecolor="white")
|
||||
ax4.axvline(0, color="black", linewidth=0.8)
|
||||
mu, sigma = daily_r.mean(), daily_r.std()
|
||||
ax4.set_title(f"Daily R μ={mu:.3f} σ={sigma:.3f}")
|
||||
ax4.set_xlabel("Daily R")
|
||||
ax4.set_ylabel("Days")
|
||||
ax4.grid(True, alpha=0.3)
|
||||
|
||||
# stats annotation
|
||||
stats = (
|
||||
f"Trades: {metrics.n_trades} WR: {metrics.win_rate*100:.1f}%\n"
|
||||
f"PF: {metrics.profit_factor:.2f} Sharpe: {metrics.sharpe:.2f}\n"
|
||||
f"Return: {metrics.total_return*100:+.1f}% MDD: {metrics.max_drawdown*100:.1f}%"
|
||||
)
|
||||
fig.text(0.5, 0.01, stats, ha="center", fontsize=9,
|
||||
bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.4))
|
||||
|
||||
out = REPORTS_DIR / filename
|
||||
plt.savefig(out, dpi=150, bbox_inches="tight")
|
||||
plt.close(fig)
|
||||
return out
|
||||
Reference in New Issue
Block a user