| name | backtest-datetime-visualization |
| description | Converting backtest visualizations from bar indices/timesteps to actual datetime axes for clearer time context |
| author | Claude Code |
| date | Sat Dec 13 2025 00:00:00 GMT+0000 (Coordinated Universal Time) |
backtest-datetime-visualization - Research Notes
Experiment Overview
| Item | Details |
|---|---|
| Date | 2025-12-13 |
| Goal | Convert backtest result visualizations from using bar indices (0, 1, 2...) to actual datetime values for equity curves, drawdowns, and trade distributions |
| Environment | Python 3.10, matplotlib, pandas, Jupyter notebooks |
| Status | Success |
Context
Backtest visualizations often use bar indices or timestep numbers on the x-axis, which makes it difficult to correlate results with actual market periods. Converting to datetime axes provides:
- Clear understanding of when drawdowns occurred
- Correlation with known market events
- Proper time-proportional spacing
Verified Workflow
1. Equity Curve with DateTime Index
When equity_curve is a pandas Series with DatetimeIndex:
# equity_series is pd.Series with DatetimeIndex
ax.plot(equity_series.index, equity_series.values, 'b-', label='Strategy')
ax.set_xlabel('Date')
ax.tick_params(axis='x', rotation=45) # Rotate for readability
2. Drawdown Plot with Timestamps
# Extract timestamps from equity series
equity = equity_series.values
timestamps = equity_series.index # DatetimeIndex
running_max = np.maximum.accumulate(equity)
drawdown = (running_max - equity) / running_max * 100
# Use timestamps for x-axis
ax.fill_between(timestamps, 0, drawdown, color='red', alpha=0.5)
ax.set_xlabel('Date')
ax.tick_params(axis='x', rotation=45)
ax.invert_yaxis() # Drawdown goes down
3. Trade P&L with Entry Times
For trade distributions, use entry_time from TradeRecord:
if len(result.trades) > 0:
trade_pnls = [t.pnl for t in result.trades]
trade_times = [t.entry_time for t in result.trades]
# Use stem plot for datetime x-axis (more robust than bar)
markerline, stemlines, baseline = ax.stem(trade_times, trade_pnls, basefmt='k-')
# Color stems based on profit/loss
for stem, pnl in zip(stemlines, trade_pnls):
stem.set_color('green' if pnl > 0 else 'red')
stem.set_alpha(0.7)
ax.set_xlabel('Date')
ax.tick_params(axis='x', rotation=45)
4. Bar Charts with DateTime (Alternative)
If you must use bar charts with datetime:
import matplotlib.dates as mdates
# Convert datetime to matplotlib date numbers
trade_dates = mdates.date2num(trade_times)
width = 0.5 # Width in days
ax.bar(trade_dates, trade_pnls, width=width, color=colors)
ax.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m-%d'))
ax.xaxis.set_major_locator(mdates.AutoDateLocator())
Failed Attempts (Critical)
| Attempt | Why it Failed | Lesson Learned |
|---|---|---|
ax.bar(trade_times, trade_pnls) directly |
Width parameter issues with datetime objects | Use stem plot or convert to matplotlib date numbers |
range(len(drawdown)) for x-axis |
Loses all time context | Always use equity_series.index |
| Not rotating x-axis labels | Dates overlap and become unreadable | Add ax.tick_params(axis='x', rotation=45) |
Using plt.xticks(rotation=45) |
Affects all subplots | Use ax.tick_params() for specific axis |
Final Parameters
# Standard pattern for backtest visualization
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
# Equity curve
ax = axes[0, 0]
ax.plot(equity_series.index, equity_series.values)
ax.set_xlabel('Date')
ax.tick_params(axis='x', rotation=45)
# Drawdown
ax = axes[0, 1]
ax.fill_between(equity_series.index, 0, drawdown)
ax.set_xlabel('Date')
ax.tick_params(axis='x', rotation=45)
# Trade P&L (use stem for datetime compatibility)
ax = axes[1, 0]
ax.stem(trade_times, trade_pnls, basefmt='k-')
ax.set_xlabel('Date')
ax.tick_params(axis='x', rotation=45)
plt.tight_layout() # Prevent label overlap
Key Insights
plt.stem()handles datetime x-values better thanplt.bar()without conversion- Always call
plt.tight_layout()after rotating labels to prevent clipping - Backtest DataFrames should use DatetimeIndex, not integer index
- TradeRecord objects should store
entry_timeas datetime, not bar index - For long time ranges, consider using
mdates.MonthLocator()orYearLocator() - The observation window plots (100-step windows) should keep "Time Step" labels since they represent relative position, not calendar time
Data Requirements
Ensure your backtest engine stores proper timestamps:
@dataclass
class TradeRecord:
symbol: str
entry_time: datetime # Not int!
exit_time: Optional[datetime]
entry_price: float
exit_price: Optional[float]
pnl: float
References
alpaca_trading/backtest/engine.py- TradeRecord with entry_timenotebooks/develop_branch_testing.ipynb- Visualization examples- matplotlib.dates documentation for advanced date formatting