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.
train = returns[:1000] - returns[:1000].mean()
model = wf.garch_fit(train)

# 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:])

# 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.

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.

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, and the numbers are reproducible from tests/reference/ in the repository.

Check Reference Result
Rolling mean and std, with gaps pandas 3.1e-11
Rolling std at a price level of 1e9 exact rational arithmetic 1.7e-15, 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 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.1.tar.gz (77.9 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.1-py3-none-win_amd64.whl (38.6 kB view details)

Uploaded Python 3Windows x86-64

walkforward-3.4.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl (35.5 kB view details)

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

walkforward-3.4.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl (34.1 kB view details)

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

walkforward-3.4.1-py3-none-macosx_11_0_arm64.whl (33.9 kB view details)

Uploaded Python 3macOS 11.0+ ARM64

walkforward-3.4.1-py3-none-macosx_10_13_x86_64.whl (36.2 kB view details)

Uploaded Python 3macOS 10.13+ x86-64

File details

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

File metadata

  • Download URL: walkforward-3.4.1.tar.gz
  • Upload date:
  • Size: 77.9 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.1.tar.gz
Algorithm Hash digest
SHA256 ac2b2ee3f87478415eea2e495cc3505736501e05a3477ad39c2aeb873aeb532d
MD5 25c5defdbfd3248d76c8f24215db7745
BLAKE2b-256 30dae5cd3c14ccc92779487fa4dab2b3b67d8e1f6dc5b5d8206c31822ba7a4b9

See more details on using hashes here.

Provenance

The following attestation bundles were made for walkforward-3.4.1.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.1-py3-none-win_amd64.whl.

File metadata

  • Download URL: walkforward-3.4.1-py3-none-win_amd64.whl
  • Upload date:
  • Size: 38.6 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.1-py3-none-win_amd64.whl
Algorithm Hash digest
SHA256 0d4a9a7d6c385499320558b7e83a523bfc8815ef0f2a5a221a6f31f12ff87571
MD5 9128a8d8419b9284c401824e09723f95
BLAKE2b-256 303818460b0d2e94acda952cb75e7cc7428af2a7f588fbd5c26b8cb90b94fdb8

See more details on using hashes here.

Provenance

The following attestation bundles were made for walkforward-3.4.1-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.1-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.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 552d2d6437ef2f0964f85077dba52fb3c80a20bd4ed7aa2c4a2208966c9935f6
MD5 3695bc5cb5e73663eef73a50659b654c
BLAKE2b-256 3343919d4020641d04349950ce86e8f2ea5db6678cd54694cb221e72b61fa213

See more details on using hashes here.

Provenance

The following attestation bundles were made for walkforward-3.4.1-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.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl.

File metadata

File hashes

Hashes for walkforward-3.4.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl
Algorithm Hash digest
SHA256 7f3737a0c4094114d9d52fa042c3ca23607f23daa86d22eb107aacff617bf70e
MD5 e7d803c47a566e2bad1b8b26b9ae82c7
BLAKE2b-256 21071099e9569d52562855fa071d276921631686f8541c17b285598cd84db6f4

See more details on using hashes here.

Provenance

The following attestation bundles were made for walkforward-3.4.1-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.1-py3-none-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for walkforward-3.4.1-py3-none-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 03f12b6bdefa7a5f3c554a674ac2da41f2bd564467af3f8ab0860abe46919d2a
MD5 bec54e165797682736d5c4fd14d95706
BLAKE2b-256 d53304da3e5aac405a828aabda8e00bc5cdc99b2955d59ec9734296c8f946d0d

See more details on using hashes here.

Provenance

The following attestation bundles were made for walkforward-3.4.1-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.1-py3-none-macosx_10_13_x86_64.whl.

File metadata

File hashes

Hashes for walkforward-3.4.1-py3-none-macosx_10_13_x86_64.whl
Algorithm Hash digest
SHA256 59498bbcbc89c3e26bb2e8945425332a24765de343eb7b2a0a341e7fbf2e4762
MD5 31da3d0cf8e160ecba368c2701be638a
BLAKE2b-256 ba7967cd8ed48619cdec4c84b929e16b60c680a21a92ab06efa2432a2e0930f1

See more details on using hashes here.

Provenance

The following attestation bundles were made for walkforward-3.4.1-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

3.4.2

6 files

This release

3.4.1 This release

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