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.0.tar.gz (78.1 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.0-py3-none-win_amd64.whl (38.6 kB view details)

Uploaded Python 3Windows x86-64

walkforward-3.4.0-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.0-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.0-py3-none-macosx_11_0_arm64.whl (33.9 kB view details)

Uploaded Python 3macOS 11.0+ ARM64

walkforward-3.4.0-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.0.tar.gz.

File metadata

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

File hashes

Hashes for walkforward-3.4.0.tar.gz
Algorithm Hash digest
SHA256 a282706342d377e7cf4032276494e6e4303f6ccb295eea903a3354269840af2c
MD5 65a2124b21622a9b2d163b40e959f1e7
BLAKE2b-256 82e6a2b3d6047660fc848e2442503f5ad267c599139419585690f9dcca4f0a15

See more details on using hashes here.

File details

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

File metadata

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

File hashes

Hashes for walkforward-3.4.0-py3-none-win_amd64.whl
Algorithm Hash digest
SHA256 455cbcefd168eeb2e397770dbcd35b825292d9e446abe544d13951792f05dcd1
MD5 e48e73b523fe3a123b75a75978e84501
BLAKE2b-256 487493d9cdaae18fab4035f6801415d9457068c911c87b5069ce39629eeec70a

See more details on using hashes here.

File details

Details for the file walkforward-3.4.0-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.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 53d4aa213b731df814ce86be4e79e15944c4df5adf82ddd392c07702f88619c5
MD5 d701c5ccbd3c712c4697a21ab0d20b76
BLAKE2b-256 1dfcddb5630698df0ffb1e064019032ecf1456d1b5ee6ab4f75f523ae29d76a0

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for walkforward-3.4.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl
Algorithm Hash digest
SHA256 4cb230597170aff8ce13808c66c20e5bab06bf660a2de998aa3117a0938df56e
MD5 3e246d979cae997f4dda87799fba7706
BLAKE2b-256 9dd84e40698be7c7afbebef0a5a9ccdb06e26b85398e3d3a38bd6ba72a307265

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for walkforward-3.4.0-py3-none-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 790d0a74bb073d30881d781c1803cb2be4261c544ab6f42eed8063f47e05fcad
MD5 3cfa9b91e92ab9978806e6a6821a752d
BLAKE2b-256 f893ea83b45fd78210eb46bcbbcd1852a745fc283145d5f4df8ea11f3f998946

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for walkforward-3.4.0-py3-none-macosx_10_13_x86_64.whl
Algorithm Hash digest
SHA256 b7723a5b33a9db77d857727c383ee4b33cb2e47e93cb155b36be7ced40a6f27c
MD5 8ab2a912adab842e73615acf0feceb81
BLAKE2b-256 67c24c6d3793cdfcf6bfb190b39d620d8015ca2c6b20cd54766bfe460f050ab5

See more details on using hashes here.

Release history Release notifications | RSS feed

3.4.2

6 files

3.4.1

6 files

This release

3.4.0 This release

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