Skip to content

Point-in-time futures reads: get_bars(..., asof=) - #25

Merged
mspinola merged 1 commit into
mainfrom
claude/asof-vintage-reads
Aug 24, 2026
Merged

Point-in-time futures reads: get_bars(..., asof=)#25
mspinola merged 1 commit into
mainfrom
claude/asof-vintage-reads

Conversation

@mspinola

Copy link
Copy Markdown
Owner

What

get_bars(sym, tier, asof="2015-06-30") returns the futures series as it stood on that date, not today's restated one.

Additive back-adjustment re-anchors to whatever contract is front now, so every past price shifts on every roll. end= truncates today's restated series; asof= also removes the cumulative spread of every roll since.

Why it matters

Only for ratio-based logic, and there it is decisive. Measured on HE against a June-2015 as-of:

signal days differing from today's series
50/200 moving-average cross 0 of 9,237
20-day breakout 0 of 9,237
>5% above the 200-day mean 1,969 (21.3%)
20-day ROC >10% 2,314 (25.1%)

A constant offset cancels out of a comparison between two points on one series, and does not cancel out of a ratio between them. So a moving-average system is immune to this and a percent-threshold system is not: a quarter of the ROC signals a backtest sees on hogs were not visible when they supposedly fired.

How

The arithmetic collapses. The offset B - U is the cumulative spread of every roll after t, so the vintage current on a past date is today's series minus the single constant offset(asof). Re-anchoring is one subtraction.

Verified two ways:

  • the stored offset is piecewise constant, zero non-roll movement across 11,714 HE bars
  • the implementation agrees with an independent roll-by-roll reconstruction to 1e-4 (HE) and 3e-3 (GC), which is float32 storage precision at those price levels

All three tiers honour it:

  • unadj is as-traded and does not restate, so its vintage is a plain truncation
  • backadj re-anchors, and raises if unadj is absent since the offset is the difference between them
  • propadj re-anchors before ratio-adjusting; the other order scales segments by roll ratios from rolls that had not happened yet

Equities raise NotImplementedError. Deliberate scope call: stored yfinance OHLC is already split-adjusted using splits after the as-of date, so the equity vintage is a different derivation, not the same one with a different date. Refusing beats ignoring the argument, since silently returning today's series is the failure the parameter exists to prevent. Follow-up if wanted.

Compatibility

asof defaults to None, so every existing read is byte-identical.

Docs (no behaviour change)

  • README, "What a futures bar carries" documents the per-bar columns for the first time, including Delivery Month and FirstContract / SecondContract, which name the actual expiries and appeared in no document before. Includes a runnable reproducer for the table above.
  • design.md, "Known holes" records that per-expiry OHLC is absent by selection, not vendor limit: _reconstruct_volume already downloads full OHLCV for every individual contract and then keeps only Date, Volume, Symbol. Same shape as the reconstruction-columns defect recorded above it, one level deeper.

Tests

11 new, on a hand-checkable fixture (three segments, roll spreads of 8, so expected vintage prices are arithmetic you can verify by eye). The headline test asserts both halves at once: difference signals invariant, ratio signals not.

242 passed, 12 skipped, ruff check src tests clean on 0.15.22.

🤖 Generated with Claude Code

Additive back-adjustment re-anchors to whatever contract is front NOW, so
every past price shifts on every roll and a study reads prices that were on
no screen at the time. `end=` truncates today's restated series; `asof=`
also removes the cumulative spread of every roll since, returning the
vintage that was current then.

The arithmetic is exact rather than approximate. The offset B - U is the
cumulative spread of every roll after t, so the vintage current on a past
date is today's series minus the single constant offset(asof). Measured on
the live store, that offset is piecewise constant with zero non-roll
movement across 11,714 HE bars, and the implementation agrees with an
independent roll-by-roll reconstruction to 1e-4 on HE and 3e-3 on GC, which
is float32 storage precision at those price levels.

It matters only for RATIO-based logic, and there it is decisive. On HE
against a June-2015 as-of, 25.1% of "20-day ROC above 10%" days and 21.3%
of "5% above the 200-day mean" days differ from today's series, while a
50/200 crossover and a 20-day breakout differ on ZERO days of 9,237: a
constant cancels out of a comparison between two points on one series and
does not cancel out of a ratio between them.

All three futures tiers honour it. `unadj` is as-traded and does not
restate, so its vintage is a plain truncation. `backadj` re-anchors, and
raises if `unadj` is absent since the offset IS the difference between
them. `propadj` re-anchors BEFORE ratio-adjusting, because the other order
scales segments by roll ratios from rolls that had not happened yet.

Equities raise NotImplementedError. The equity vintage is a different
derivation, not the same one with a different date: stored yfinance OHLC is
already split-adjusted using splits AFTER the as-of date. Refusing beats
ignoring the argument, since silently returning today's series for a
point-in-time request is the failure this parameter exists to prevent.

asof defaults to None, so every existing read is byte-identical.

Docs, no behaviour change:

- README "What a futures bar carries" documents the per-bar columns for the
  first time, including Delivery Month and FirstContract / SecondContract,
  which name the actual expiries and appeared in no document before.
- design.md "Known holes" records that per-expiry OHLC is absent by
  SELECTION, not vendor limit: _reconstruct_volume already downloads full
  OHLCV for every individual contract and then keeps only Date, Volume and
  Symbol. Same shape as the reconstruction-columns defect recorded above
  it, one level deeper.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@mspinola
mspinola merged commit 7d99f8e into main Aug 24, 2026
5 checks passed
@mspinola
mspinola deleted the claude/asof-vintage-reads branch August 24, 2026 23:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant