diff --git a/.husky/commit-msg b/.husky/commit-msg
index dd711a8..6e57fd8 100644
Binary files a/.husky/commit-msg and b/.husky/commit-msg differ
diff --git a/src/quant_research_starter/backtest/__init__.py b/src/quant_research_starter/backtest/__init__.py
index c32e8c8..98647cf 100644
--- a/src/quant_research_starter/backtest/__init__.py
+++ b/src/quant_research_starter/backtest/__init__.py
@@ -1,4 +1,4 @@
-"""Backtest module public API."""
+"""Backtesting engines and portfolio simulation."""
from .vectorized import VectorizedBacktest
diff --git a/src/quant_research_starter/cli.py b/src/quant_research_starter/cli.py
index 2fdb8a6..18ca8c1 100644
--- a/src/quant_research_starter/cli.py
+++ b/src/quant_research_starter/cli.py
@@ -10,7 +10,7 @@
from .backtest import VectorizedBacktest
from .data import SampleDataLoader, SyntheticDataGenerator
from .factors import MomentumFactor, SizeFactor, ValueFactor, VolatilityFactor
-from .metrics import RiskMetrics
+from .metrics import RiskMetrics, create_equity_curve_plot
@click.group()
@@ -127,7 +127,13 @@ def compute_factors(data_file, factors, output):
help="Output file for results",
)
@click.option("--plot/--no-plot", default=True, help="Generate plot")
-def backtest(data_file, signals_file, initial_capital, output, plot):
+@click.option(
+ "--plotly",
+ is_flag=True,
+ default=False,
+ help="Also generate interactive Plotly HTML chart",
+)
+def backtest(data_file, signals_file, initial_capital, output, plot, plotly):
"""Run backtest with given signals."""
click.echo("Running backtest...")
@@ -214,6 +220,20 @@ def backtest(data_file, signals_file, initial_capital, output, plot):
click.echo(f"Plot saved -> {plot_path}")
+ # Generate Plotly HTML chart if requested
+ if plotly:
+ html_path = output_path.parent / "backtest_plot.html"
+
+ create_equity_curve_plot(
+ dates=results_dict["dates"],
+ portfolio_values=results_dict["portfolio_value"],
+ initial_capital=initial_capital,
+ output_path=str(html_path),
+ plot_type="html",
+ )
+
+ click.echo(f"Plotly HTML chart saved -> {html_path}")
+
click.echo("Backtest completed!")
click.echo(f"Final portfolio value: ${results['final_value']:,.2f}")
click.echo(f"Total return: {results['total_return']:.2%}")
diff --git a/src/quant_research_starter/metrics/__init__.py b/src/quant_research_starter/metrics/__init__.py
index b2b4850..6438e82 100644
--- a/src/quant_research_starter/metrics/__init__.py
+++ b/src/quant_research_starter/metrics/__init__.py
@@ -1,5 +1,6 @@
"""Metrics module public API."""
+from .plotting import create_equity_curve_plot
from .risk import RiskMetrics
-__all__ = ["RiskMetrics"]
+__all__ = ["create_equity_curve_plot", "RiskMetrics"]
diff --git a/src/quant_research_starter/metrics/plotting.py b/src/quant_research_starter/metrics/plotting.py
new file mode 100644
index 0000000..1baa26c
--- /dev/null
+++ b/src/quant_research_starter/metrics/plotting.py
@@ -0,0 +1,94 @@
+"""Plotting utilities for backtest results."""
+
+from pathlib import Path
+from typing import List
+
+import pandas as pd
+
+try:
+ import plotly.graph_objects as go
+
+ PLOTLY_AVAILABLE = True
+except ImportError:
+ PLOTLY_AVAILABLE = False
+
+import matplotlib.pyplot as plt
+
+
+def create_equity_curve_plot(
+ dates: List[str],
+ portfolio_values: List[float],
+ initial_capital: float,
+ output_path: str,
+ plot_type: str = "png",
+) -> str:
+ """
+ Create equity curve plot in specified format.
+ Args:
+ dates: List of date strings
+ portfolio_values: List of portfolio values
+ initial_capital: Starting capital
+ output_path: Path to save the plot
+ plot_type: 'png' for matplotlib, 'html' for plotly
+
+ Returns:
+ Path to saved file
+ """
+ Path(output_path).parent.mkdir(parents=True, exist_ok=True)
+ dates = pd.to_datetime(dates)
+ portfolio_series = pd.Series(portfolio_values, index=dates)
+ if plot_type == "html" and PLOTLY_AVAILABLE:
+ return _create_plotly_chart(portfolio_series, initial_capital, output_path)
+ else:
+ return _create_matplotlib_chart(portfolio_series, output_path)
+
+
+def _create_plotly_chart(
+ portfolio_series: pd.Series, initial_capital: float, output_path: str
+) -> str:
+ """Create interactive Plotly HTML chart."""
+ fig = go.Figure()
+ fig.add_trace(
+ go.Scatter(
+ x=portfolio_series.index,
+ y=portfolio_series.values,
+ mode="lines",
+ name="Portfolio Value",
+ line={"color": "#2E86AB", "width": 3},
+ hovertemplate="Date: %{x|%Y-%m-%d}
Value: $%{y:,.2f}",
+ )
+ )
+ # Add initial capital reference line
+ fig.add_hline(
+ y=initial_capital,
+ line_dash="dash",
+ line_color="red",
+ annotation_text=f"Initial Capital: ${initial_capital:,.0f}",
+ annotation_position="bottom right",
+ )
+ total_return_pct = ((portfolio_series.iloc[-1] / initial_capital) - 1) * 10
+ fig.update_layout(
+ title=f"Backtest Performance (Total Return: {total_return_pct:+.1f}%)",
+ xaxis_title="Date",
+ yaxis_title="Portfolio Value ($)",
+ template="plotly_white",
+ hovermode="x unified",
+ height=500,
+ )
+ fig.update_yaxes(tickprefix="$", tickformat=",.0f")
+ fig.write_html(output_path)
+ return output_path
+
+
+def _create_matplotlib_chart(portfolio_series: pd.Series, output_path: str) -> str:
+ """Create static matplotlib PNG chart."""
+ plt.figure(figsize=(10, 6))
+ plt.plot(portfolio_series.index, portfolio_series.values, linewidth=2)
+ plt.title("Portfolio Value")
+ plt.ylabel("USD")
+ plt.grid(True, alpha=0.3)
+ plt.gcf().autofmt_xdate()
+ plt.tight_layout()
+ plt.savefig(output_path, dpi=150, bbox_inches="tight")
+ plt.close()
+ return output_path
diff --git a/tests/test_plotting.py b/tests/test_plotting.py
new file mode 100644
index 0000000..45a6a3d
--- /dev/null
+++ b/tests/test_plotting.py
@@ -0,0 +1,55 @@
+import os
+
+from quant_research_starter.metrics.plotting import create_equity_curve_plot
+
+
+def test_plotly_html_creation():
+ # Create test data matching the backtest results structure
+ dates = [f"2020-01-{i+1:02d}" for i in range(20)]
+ portfolio_values = [1000000 + i * 5000 for i in range(20)]
+
+ # Test HTML creation
+ test_html_path = "test_output/backtest_plot.html"
+ html_path = create_equity_curve_plot(
+ dates=dates,
+ portfolio_values=portfolio_values,
+ initial_capital=1000000,
+ output_path=test_html_path,
+ plot_type="html",
+ )
+
+ # Verify file was created
+ assert os.path.exists(html_path)
+ assert html_path.endswith(".html")
+ assert os.path.getsize(html_path) > 1000
+
+ # Cleanup
+ if os.path.exists(html_path):
+ os.remove(html_path)
+ if os.path.exists("test_output"):
+ os.rmdir("test_output")
+
+
+def test_plotly_fallback_to_matplotlib():
+ """Test that PNG is created even if Plotly is not available."""
+ dates = [f"2020-01-{i+1:02d}" for i in range(15)]
+ portfolio_values = [1000000 + i * 3000 for i in range(15)]
+
+ # Test PNG creation
+ test_png_path = "test_output/backtest_plot.png"
+ png_path = create_equity_curve_plot(
+ dates=dates,
+ portfolio_values=portfolio_values,
+ initial_capital=1000000,
+ output_path=test_png_path,
+ plot_type="png",
+ )
+
+ assert os.path.exists(png_path)
+ assert png_path.endswith(".png")
+
+ # Cleanup
+ if os.path.exists(png_path):
+ os.remove(png_path)
+ if os.path.exists("test_output"):
+ os.rmdir("test_output")