Skip to content

fix: prevent benchmark alignment look-ahead - #557

Open
JohnDReynolds wants to merge 1 commit into
ranaroussi:mainfrom
JohnDReynolds:fix/pr-1/prevent-benchmark-lookahead
Open

JohnDReynolds wants to merge 1 commit into
ranaroussi:mainfrom
JohnDReynolds:fix/pr-1/prevent-benchmark-lookahead

Conversation

@JohnDReynolds

@JohnDReynolds JohnDReynolds commented Sep 28, 2026 •

Copy link
Copy Markdown

Summary

Prevent benchmark alignment from moving a future benchmark return onto an earlier strategy date.

When a strategy calendar contains dates missing from the benchmark, _prepare_benchmark() reconstructs benchmark prices on a daily index before converting them back to returns. Back-filling that price index allowed a later observed benchmark price to propagate backward, creating look-ahead in reports, plots, and statistics that consume the aligned benchmark.

This changes the alignment from backward-fill to forward-fill so each benchmark return remains on its observation date.

Behavior

Consider a benchmark observed on Friday and Monday, with a 10% return on Monday, aligned to a daily strategy calendar:

Dates:     Friday  Saturday  Sunday  Monday
Previous:   0.00      0.10     0.00    0.00
Expected:   0.00      0.00     0.00    0.10

Back-filling assigned Monday's price to earlier weekend dates, causing the return to appear before it occurred. Forward-filling carries Friday's last known price through the weekend and preserves the return on Monday.

The change preserves:

  • already aligned benchmark data;
  • leading and trailing strategy dates;
  • positive and negative benchmark returns;
  • compounding across irregular target periods;
  • Series and one-column DataFrame inputs;
  • labels, indexes, and timezone normalization; and
  • caller-owned benchmark data.

Regression evidence

  • Fail before: A Friday/Monday benchmark aligned to a daily strategy calendar produced [0, 0.10, 0, 0] instead of [0, 0, 0, 0.10]. Through the public statistics path, information_ratio() returned -0.261116483933547 instead of the independently calculated -0.3100868364730212.
  • Independent expectation: Alignment must be causal: a benchmark price may be carried forward from its last observation, but a later price must never be propagated to an earlier date.
  • Pass after: The return remains on Monday for either sign; leading and trailing dates remain flat; intervening irregular-period returns compound to 0.32; timezone-aware DataFrame alignment preserves labels and caller state; and information_ratio() matches its independent active-return calculation.

Verification

  • 5 focused causal-alignment tests passed.
  • 152 utility, statistics, and regression tests passed.
  • 35 downstream report and plot tests passed.
  • 230 full-suite tests passed with 60 inherited seaborn/Matplotlib deprecation warnings.
  • Changed-line and full Ruff lint passed.
  • Ruff formatting and git diff --check passed.
  • No public signature, documentation, dependency, packaging, or build behavior changed.

Scope

Changed files:

  • quantstats/utils.py
  • tests/test_regressions.py

The production change is limited to replacing backward price filling with forward price filling inside _prepare_benchmark().

Out of scope:

  • changing benchmark statistic formulas;
  • redesigning end-of-year reports;
  • changing benchmark downloads; and
  • unrelated alignment or formatting cleanup.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

1 participant