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
tis built from data beforet.ewma_voland the GARCH filters are predictive; the rolling statistics and range estimators are contemporaneous and say so in their own docstrings, withlagto fix them. - Purging that asks the right question.
PurgedWalkForwardtakes a label horizon, not a purge count, becausepurge = h - 1is the part people get wrong. Drops straight intocross_val_score. - GARCH(1,1) by maximum likelihood, checked against
archon 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
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
312cd92eddfcd728c16a8822c4353a4d7067848019504781c2b02ca3ee7c5da3
|
|
| MD5 |
acb80f85b09e6cf999b5d62cd11ea333
|
|
| BLAKE2b-256 |
54d40f61d02eb90740cf06daacd606ecac3d17a77cc83a848db8919a1477c935
|
Provenance
The following attestation bundles were made for walkforward-3.4.2.tar.gz:
Publisher:
release.yml on haeganm/walkforward
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
walkforward-3.4.2.tar.gz -
Subject digest:
312cd92eddfcd728c16a8822c4353a4d7067848019504781c2b02ca3ee7c5da3 - Sigstore transparency entry: 2732609256
- Sigstore integration time:
-
Permalink:
haeganm/walkforward@173420d2080eb5fb759ee5de619992fd07df15c0 -
Branch / Tag:
refs/tags/v3.4.2 - Owner: https://github.com/haeganm
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@173420d2080eb5fb759ee5de619992fd07df15c0 -
Trigger Event:
push
-
Statement type:
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fb64141dd5f3bb911ce5c665127191ca2c6e392f43f0110a12f64e905526531a
|
|
| MD5 |
e3b0ba025c0b4a6f27f04fdeeb27694b
|
|
| BLAKE2b-256 |
30950b91fe283d3ea2a9a83066dc40e3f878db557b8008c97c82932cc8d321fe
|
Provenance
The following attestation bundles were made for walkforward-3.4.2-py3-none-win_amd64.whl:
Publisher:
release.yml on haeganm/walkforward
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
walkforward-3.4.2-py3-none-win_amd64.whl -
Subject digest:
fb64141dd5f3bb911ce5c665127191ca2c6e392f43f0110a12f64e905526531a - Sigstore transparency entry: 2732609275
- Sigstore integration time:
-
Permalink:
haeganm/walkforward@173420d2080eb5fb759ee5de619992fd07df15c0 -
Branch / Tag:
refs/tags/v3.4.2 - Owner: https://github.com/haeganm
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@173420d2080eb5fb759ee5de619992fd07df15c0 -
Trigger Event:
push
-
Statement type:
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
- Download URL: walkforward-3.4.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl
- Upload date:
- Size: 38.6 kB
- Tags: Python 3, manylinux: glibc 2.17+ x86-64, manylinux: glibc 2.28+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b29874c2d06efe7c9dc187f8bc25dc996389a3f8fdb889255085470fbd7d1aec
|
|
| MD5 |
2e66f1dd544b9ca9e310ba5c9c63ae2e
|
|
| BLAKE2b-256 |
ec535d80d69c20e2dbf14227728e681dcb968958b9bf9c86d2283c8892e9a074
|
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
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
walkforward-3.4.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl -
Subject digest:
b29874c2d06efe7c9dc187f8bc25dc996389a3f8fdb889255085470fbd7d1aec - Sigstore transparency entry: 2732609265
- Sigstore integration time:
-
Permalink:
haeganm/walkforward@173420d2080eb5fb759ee5de619992fd07df15c0 -
Branch / Tag:
refs/tags/v3.4.2 - Owner: https://github.com/haeganm
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@173420d2080eb5fb759ee5de619992fd07df15c0 -
Trigger Event:
push
-
Statement type:
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
- Download URL: walkforward-3.4.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl
- Upload date:
- Size: 37.2 kB
- Tags: Python 3, manylinux: glibc 2.17+ ARM64, manylinux: glibc 2.28+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
16b91ff6312c5c2910b2a59f622f40140e5609b003b8cdcf2303664948df665d
|
|
| MD5 |
5a3b783390ef060fb59399918636d4b7
|
|
| BLAKE2b-256 |
6e00a999134a26a4da8d28b5f43690c551cdd930f42cf5170f0d6deab166bc59
|
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
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
walkforward-3.4.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl -
Subject digest:
16b91ff6312c5c2910b2a59f622f40140e5609b003b8cdcf2303664948df665d - Sigstore transparency entry: 2732609263
- Sigstore integration time:
-
Permalink:
haeganm/walkforward@173420d2080eb5fb759ee5de619992fd07df15c0 -
Branch / Tag:
refs/tags/v3.4.2 - Owner: https://github.com/haeganm
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@173420d2080eb5fb759ee5de619992fd07df15c0 -
Trigger Event:
push
-
Statement type:
File details
Details for the file walkforward-3.4.2-py3-none-macosx_11_0_arm64.whl.
File metadata
- Download URL: walkforward-3.4.2-py3-none-macosx_11_0_arm64.whl
- Upload date:
- Size: 37.0 kB
- Tags: Python 3, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
15013bbfcb9c22158b1efa812ce26606d0471c464dd2f8b4d1bb7c3400088cb5
|
|
| MD5 |
2839a73f518f30257d95885d778bd31e
|
|
| BLAKE2b-256 |
72f56afeec65538b76df652f8150464e2fba111359801ebf27998364b765546e
|
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
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
walkforward-3.4.2-py3-none-macosx_11_0_arm64.whl -
Subject digest:
15013bbfcb9c22158b1efa812ce26606d0471c464dd2f8b4d1bb7c3400088cb5 - Sigstore transparency entry: 2732609266
- Sigstore integration time:
-
Permalink:
haeganm/walkforward@173420d2080eb5fb759ee5de619992fd07df15c0 -
Branch / Tag:
refs/tags/v3.4.2 - Owner: https://github.com/haeganm
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@173420d2080eb5fb759ee5de619992fd07df15c0 -
Trigger Event:
push
-
Statement type:
File details
Details for the file walkforward-3.4.2-py3-none-macosx_10_13_x86_64.whl.
File metadata
- Download URL: walkforward-3.4.2-py3-none-macosx_10_13_x86_64.whl
- Upload date:
- Size: 39.5 kB
- Tags: Python 3, macOS 10.13+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
40ccf861fe57d1f87adaee3832410080f9a0a91f48948e5e4f17870fa37a6f8e
|
|
| MD5 |
5d0f6c9426c6d7810bd9ec6750fba483
|
|
| BLAKE2b-256 |
8ab44a729e51ae3c0986a28d6e2cad59a3787324112855e7eaa3015eec9a663a
|
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
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
walkforward-3.4.2-py3-none-macosx_10_13_x86_64.whl -
Subject digest:
40ccf861fe57d1f87adaee3832410080f9a0a91f48948e5e4f17870fa37a6f8e - Sigstore transparency entry: 2732609269
- Sigstore integration time:
-
Permalink:
haeganm/walkforward@173420d2080eb5fb759ee5de619992fd07df15c0 -
Branch / Tag:
refs/tags/v3.4.2 - Owner: https://github.com/haeganm
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@173420d2080eb5fb759ee5de619992fd07df15c0 -
Trigger Event:
push
-
Statement type: