Skip to main content

walkforward

Volatility forecasts, position sizing and purged walk-forward splits that never see the future. A C11 library with a numpy front door.

pip install walkforward

Most backtests that look good are quietly peeking one bar ahead. The estimator that sizes a position knows the return it is about to earn, or a training label was computed from prices inside the test window. This library is built so those two things cannot happen by accident, and it is checked on every push against pandas, numpy, scikit-learn, the arch package and exact rational arithmetic.

  • One timing convention. Every forecast at index t is built from data before t. ewma_vol and the GARCH filters are predictive; the rolling statistics and range estimators are contemporaneous and say so in their own docstrings, with lag to fix them.
  • Purging that asks the right question. PurgedWalkForward takes a label horizon, not a purge count, because purge = h - 1 is the part people get wrong. Drops straight into cross_val_score.
  • GARCH(1,1) by maximum likelihood, checked against arch on identical samples and identical likelihoods, and invariant to the units of the returns.
  • Sizing that fails closed. A bad price gives a zero position, not a NaN one. A leverage cap that could overflow is refused rather than silently ignored.
  • Ridge by Householder QR, so a feature at a price level near 1e9 keeps its slope precision.

Research tooling, not investment advice. It is a set of building blocks, not a backtester: it knows nothing about costs, borrow, calendars or corporate actions.

The loop

import numpy as np
import walkforward as wf

# The fit assumes mean-zero returns. Demean with the TRAINING mean; the
# full-sample mean would put the future into the fit.
mu = returns[:1000].mean()
model = wf.garch_fit(returns[:1000] - mu)

# sigma[t] forecasts period t from returns before t. filter_from continues
# from the variance state the fit ended on, which is what makes the
# out-of-sample path the same as filtering everything together.
sigma = model.filter_from(returns[1000:] - mu)

# A position held over period t is entered at the close of t-1, so it is
# sized against the previous close, and earns position * price * return.
entry = close[999:-1]
position = wf.vol_target_position(
    sigma, target_vol=0.01, equity=100_000.0, price=entry, max_leverage=2.0
)
pnl = position * entry * returns[1000:]

model.persistence, model.half_life and model.unconditional_vol are there so you do not have to recompute them.

Cross-validation

from sklearn.ensemble import GradientBoostingRegressor
from sklearn.model_selection import cross_val_score
import walkforward as wf

# Target is a 21-period forward return, so labels span 21 periods and the
# last 20 training rows before each test window are dropped.
cv = wf.PurgedWalkForward(train_size=756, test_size=252, label_horizon=21)

scores = cross_val_score(GradientBoostingRegressor(), X, y, cv=cv)

Without the purge, the twenty training rows before each test window carry labels that were partly computed from test-window prices. The library counts that leak in its own test suite: at a horizon of 21, a purge of 20 leaves zero leaking training rows and a purge of 19 leaves ten.

There is no embargo argument, on purpose. An embargo protects training data that sits after a test window, and a walk-forward never trains on anything after the window it is testing. walk_forward_splits(..., include_post_train=True) exposes that variant with the warning it deserves.

A full example

examples/spy_walk_forward.ipynb runs one model end to end on 25 years of daily SPY: lagged features, a ridge on a five-day forward return, 41 purged folds, a GARCH per fold continued with filter_from, and 10% vol targeting sized against the previous close. Vol targeting alone lifts the out-of-sample Sharpe from 0.63 to 0.79 and halves the drawdown; the ridge signal has no edge and the notebook says so. The same model with the lags removed reports a Sharpe of 14.6, which is the leak this package exists to make hard.

Conventions

Volatility is per period. Annualised converts as annual / sqrt(periods_per_year), so a 10% annual target on daily data is 0.10 / sqrt(252), about 0.0063.

rolling_std uses the population convention and divides by window. Pandas rolling().std() defaults to the sample convention, so the two differ by sqrt(window / (window - 1)).

A pandas Series in gives a pandas Series out, on the same index. Realigning a bare array by hand is one of the ways lookahead gets in, so two Series passed together must share an index; lag is the way to shift one.

Bad arguments raise. Bad elements are handled per function and documented on each: a non-finite return is skipped by the recursions, makes a rolling window NaN, and gives a zero position in sizing. DomainError (a ValueError) means the arguments were fine but the computation has no answer: a singular design, a sample with no variance, an overflowing recursion.

What it is checked against

The C library underneath is compared on every push to independent implementations. The numbers below come from tests/reference/reference_check.py in the repository, which needs only the packages it names.

Check Reference Result
Rolling mean and std, with gaps pandas 3.1e-11
Rolling std at a price level of 1e9 exact rational arithmetic 4.4e-16, where a two-pass computation is off by 4.9e-12
EWMA, predictive alignment pandas ewm shifted one period 3.5e-18
GARCH filter and forecast arch 4.4e-16 and 8.9e-16 relative
GARCH fit over 20 samples arch, same likelihood 5.0e-7 max parameter difference
Ridge, condition number 1e4 to 1e10 SVD least squares within 2x condition times epsilon
Ridge with a feature at level 1e6 to 1e15 exact rational OLS under 1e-14
Walk-forward splits, 1620 parameter sets independent generator 0 mismatches
No lookahead, 40 trials bitwise prefix comparison 0 violations

The C also runs under AddressSanitizer and UndefinedBehaviorSanitizer, on 32-bit and 64-bit, and gives bit-identical answers under GCC and Clang.

License

MIT. Source and the C library: https://github.com/haeganm/walkforward

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

walkforward-3.4.2.tar.gz (92.3 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

walkforward-3.4.2-py3-none-win_amd64.whl (41.9 kB view details)

Uploaded Python 3Windows x86-64

walkforward-3.4.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl (38.6 kB view details)

Uploaded Python 3manylinux: glibc 2.17+ x86-64manylinux: glibc 2.28+ x86-64

walkforward-3.4.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl (37.2 kB view details)

Uploaded Python 3manylinux: glibc 2.17+ ARM64manylinux: glibc 2.28+ ARM64

walkforward-3.4.2-py3-none-macosx_11_0_arm64.whl (37.0 kB view details)

Uploaded Python 3macOS 11.0+ ARM64

walkforward-3.4.2-py3-none-macosx_10_13_x86_64.whl (39.5 kB view details)

Uploaded Python 3macOS 10.13+ x86-64

File details

Details for the file walkforward-3.4.2.tar.gz.

File metadata

  • Download URL: walkforward-3.4.2.tar.gz
  • Upload date:
  • Size: 92.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for walkforward-3.4.2.tar.gz
Algorithm Hash digest
SHA256 312cd92eddfcd728c16a8822c4353a4d7067848019504781c2b02ca3ee7c5da3
MD5 acb80f85b09e6cf999b5d62cd11ea333
BLAKE2b-256 54d40f61d02eb90740cf06daacd606ecac3d17a77cc83a848db8919a1477c935

See more details on using hashes here.

Provenance

The following attestation bundles were made for walkforward-3.4.2.tar.gz:

Publisher: release.yml on haeganm/walkforward

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file walkforward-3.4.2-py3-none-win_amd64.whl.

File metadata

  • Download URL: walkforward-3.4.2-py3-none-win_amd64.whl
  • Upload date:
  • Size: 41.9 kB
  • Tags: Python 3, Windows x86-64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for walkforward-3.4.2-py3-none-win_amd64.whl
Algorithm Hash digest
SHA256 fb64141dd5f3bb911ce5c665127191ca2c6e392f43f0110a12f64e905526531a
MD5 e3b0ba025c0b4a6f27f04fdeeb27694b
BLAKE2b-256 30950b91fe283d3ea2a9a83066dc40e3f878db557b8008c97c82932cc8d321fe

See more details on using hashes here.

Provenance

The following attestation bundles were made for walkforward-3.4.2-py3-none-win_amd64.whl:

Publisher: release.yml on haeganm/walkforward

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file walkforward-3.4.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl.

File metadata

File hashes

Hashes for walkforward-3.4.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 b29874c2d06efe7c9dc187f8bc25dc996389a3f8fdb889255085470fbd7d1aec
MD5 2e66f1dd544b9ca9e310ba5c9c63ae2e
BLAKE2b-256 ec535d80d69c20e2dbf14227728e681dcb968958b9bf9c86d2283c8892e9a074

See more details on using hashes here.

Provenance

The following attestation bundles were made for walkforward-3.4.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl:

Publisher: release.yml on haeganm/walkforward

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file walkforward-3.4.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl.

File metadata

File hashes

Hashes for walkforward-3.4.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl
Algorithm Hash digest
SHA256 16b91ff6312c5c2910b2a59f622f40140e5609b003b8cdcf2303664948df665d
MD5 5a3b783390ef060fb59399918636d4b7
BLAKE2b-256 6e00a999134a26a4da8d28b5f43690c551cdd930f42cf5170f0d6deab166bc59

See more details on using hashes here.

Provenance

The following attestation bundles were made for walkforward-3.4.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl:

Publisher: release.yml on haeganm/walkforward

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file walkforward-3.4.2-py3-none-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for walkforward-3.4.2-py3-none-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 15013bbfcb9c22158b1efa812ce26606d0471c464dd2f8b4d1bb7c3400088cb5
MD5 2839a73f518f30257d95885d778bd31e
BLAKE2b-256 72f56afeec65538b76df652f8150464e2fba111359801ebf27998364b765546e

See more details on using hashes here.

Provenance

The following attestation bundles were made for walkforward-3.4.2-py3-none-macosx_11_0_arm64.whl:

Publisher: release.yml on haeganm/walkforward

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file walkforward-3.4.2-py3-none-macosx_10_13_x86_64.whl.

File metadata

File hashes

Hashes for walkforward-3.4.2-py3-none-macosx_10_13_x86_64.whl
Algorithm Hash digest
SHA256 40ccf861fe57d1f87adaee3832410080f9a0a91f48948e5e4f17870fa37a6f8e
MD5 5d0f6c9426c6d7810bd9ec6750fba483
BLAKE2b-256 8ab44a729e51ae3c0986a28d6e2cad59a3787324112855e7eaa3015eec9a663a

See more details on using hashes here.

Provenance

The following attestation bundles were made for walkforward-3.4.2-py3-none-macosx_10_13_x86_64.whl:

Publisher: release.yml on haeganm/walkforward

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

3.4.2 This release

6 files

3.4.1

6 files

3.4.0

6 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page