Back to skills
extension
Category: Development & EngineeringNo API key required

backtest-datetime-visualization

Converting backtest visualizations from bar indices/timesteps to actual datetime axes for clearer time context

personAuthor: jakexiaohubgithub

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 than plt.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_time as datetime, not bar index
  • For long time ranges, consider using mdates.MonthLocator() or YearLocator()
  • 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_time
  • notebooks/develop_branch_testing.ipynb - Visualization examples
  • matplotlib.dates documentation for advanced date formatting