walkforward 3.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- walkforward-3.4.0/.gitattributes +3 -0
- walkforward-3.4.0/.gitignore +28 -0
- walkforward-3.4.0/CHANGELOG.md +372 -0
- walkforward-3.4.0/CMakeLists.txt +112 -0
- walkforward-3.4.0/LICENSE +21 -0
- walkforward-3.4.0/PKG-INFO +121 -0
- walkforward-3.4.0/README.md +215 -0
- walkforward-3.4.0/RELEASING.md +82 -0
- walkforward-3.4.0/cmake/mlrisk.pc.in +9 -0
- walkforward-3.4.0/cmake/mlriskConfig.cmake.in +5 -0
- walkforward-3.4.0/include/mlrisk/linreg.h +122 -0
- walkforward-3.4.0/include/mlrisk/mlrisk.h +17 -0
- walkforward-3.4.0/include/mlrisk/rolling.h +94 -0
- walkforward-3.4.0/include/mlrisk/sizing.h +117 -0
- walkforward-3.4.0/include/mlrisk/split.h +98 -0
- walkforward-3.4.0/include/mlrisk/types.h +70 -0
- walkforward-3.4.0/include/mlrisk/version.h.in +24 -0
- walkforward-3.4.0/include/mlrisk/vol.h +205 -0
- walkforward-3.4.0/pyproject.toml +100 -0
- walkforward-3.4.0/python/.gitignore +5 -0
- walkforward-3.4.0/python/CMakeLists.txt +39 -0
- walkforward-3.4.0/python/README.md +94 -0
- walkforward-3.4.0/python/src/walkforward/__init__.py +86 -0
- walkforward-3.4.0/python/src/walkforward/_core.py +255 -0
- walkforward-3.4.0/python/src/walkforward/linear.py +120 -0
- walkforward-3.4.0/python/src/walkforward/rolling.py +74 -0
- walkforward-3.4.0/python/src/walkforward/sizing.py +162 -0
- walkforward-3.4.0/python/src/walkforward/split.py +255 -0
- walkforward-3.4.0/python/src/walkforward/volatility.py +316 -0
- walkforward-3.4.0/python/tests/test_walkforward.py +385 -0
- walkforward-3.4.0/src/linreg.c +226 -0
- walkforward-3.4.0/src/rolling.c +226 -0
- walkforward-3.4.0/src/sizing.c +118 -0
- walkforward-3.4.0/src/split.c +64 -0
- walkforward-3.4.0/src/version.c +9 -0
- walkforward-3.4.0/src/vol.c +399 -0
- walkforward-3.4.0/tests/test_fuzz.c +181 -0
- walkforward-3.4.0/tests/test_linreg.c +284 -0
- walkforward-3.4.0/tests/test_main.c +53 -0
- walkforward-3.4.0/tests/test_rolling.c +358 -0
- walkforward-3.4.0/tests/test_sizing.c +165 -0
- walkforward-3.4.0/tests/test_split.c +170 -0
- walkforward-3.4.0/tests/test_util.h +35 -0
- walkforward-3.4.0/tests/test_vol.c +417 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Build
|
|
2
|
+
build/
|
|
3
|
+
build-*/
|
|
4
|
+
out/
|
|
5
|
+
cmake-build-*/
|
|
6
|
+
CMakeCache.txt
|
|
7
|
+
CMakeFiles/
|
|
8
|
+
Testing/
|
|
9
|
+
compile_commands.json
|
|
10
|
+
|
|
11
|
+
# Compiled
|
|
12
|
+
*.o
|
|
13
|
+
*.obj
|
|
14
|
+
*.a
|
|
15
|
+
*.lib
|
|
16
|
+
*.so
|
|
17
|
+
*.dylib
|
|
18
|
+
*.dll
|
|
19
|
+
*.exe
|
|
20
|
+
*.pdb
|
|
21
|
+
*.dSYM/
|
|
22
|
+
|
|
23
|
+
# IDE / OS
|
|
24
|
+
.vscode/
|
|
25
|
+
.idea/
|
|
26
|
+
.vs/
|
|
27
|
+
*.swp
|
|
28
|
+
.DS_Store
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 3.4.0 (2026-09-04)
|
|
4
|
+
|
|
5
|
+
First release published to PyPI as `walkforward`. The C library is unchanged
|
|
6
|
+
from 3.3.1; everything here is the Python package and the machinery to ship
|
|
7
|
+
it. Publishing uses a PyPI API token rather than trusted publishing, because
|
|
8
|
+
GitHub issues this repository an OIDC subject claim carrying numeric owner and
|
|
9
|
+
repository identifiers that PyPI does not match; `RELEASING.md` has the
|
|
10
|
+
detail.
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- A Python package, `walkforward`, in `python/`. It binds this library with
|
|
15
|
+
ctypes and numpy: `ewma_vol`, `garch_fit` returning a `GarchModel` with
|
|
16
|
+
`persistence`, `half_life` and `unconditional_vol`, the rolling statistics
|
|
17
|
+
with a `lag` helper, the sizing functions, `Ridge`, and
|
|
18
|
+
`PurgedWalkForward`, a scikit-learn cross-validator.
|
|
19
|
+
- Two API decisions worth naming. The splitter asks for `label_horizon`
|
|
20
|
+
rather than a purge count, because `purge = h - 1` is the part users get
|
|
21
|
+
wrong, and it has no `embargo` argument, because a walk-forward never
|
|
22
|
+
trains on data after the window it is testing. `walk_forward_splits`
|
|
23
|
+
exposes the post-training variant for people who want it.
|
|
24
|
+
- The binding allocates every output buffer itself, so the `restrict`
|
|
25
|
+
non-aliasing contract on the C outputs cannot be violated by a numpy view,
|
|
26
|
+
and a pandas Series keeps its index through a call.
|
|
27
|
+
- Wheels are `py3-none`: the binding never touches the Python C API, so one
|
|
28
|
+
wheel per platform serves every Python 3.
|
|
29
|
+
- A `python-package` CI job builds and tests the wheel on Linux, macOS and
|
|
30
|
+
Windows, asserts the package and the library it bundles report the same
|
|
31
|
+
version, and installs from a source distribution in a clean directory so
|
|
32
|
+
the sdist keeps carrying the C it has to compile.
|
|
33
|
+
- The Python packaging sits at the repository root rather than in `python/`,
|
|
34
|
+
because a source distribution has to contain `src/` and `include/` and
|
|
35
|
+
those cannot be reached from a subdirectory.
|
|
36
|
+
- A `Release` workflow that builds the sdist and five wheels (Linux x86-64
|
|
37
|
+
and aarch64, macOS Intel and Apple silicon, Windows x64), tests each wheel
|
|
38
|
+
on the platform it was built for, installs the sdist from scratch, and
|
|
39
|
+
publishes to PyPI through trusted publishing, with no API token in the
|
|
40
|
+
repository. A tag that disagrees with the version in `CMakeLists.txt` is
|
|
41
|
+
refused before anything is built. See `RELEASING.md`.
|
|
42
|
+
Validated by running the workflow before any tag: it produces one
|
|
43
|
+
`py3-none` wheel per platform, each carrying its own shared library, and
|
|
44
|
+
the Linux wheels need only glibc 2.17 so they install anywhere numpy does.
|
|
45
|
+
|
|
46
|
+
### Changed
|
|
47
|
+
|
|
48
|
+
- The README is rewritten around the Python package, which is how most people
|
|
49
|
+
will use this, with the C library as its own section rather than the
|
|
50
|
+
headline. Every measured number is carried over, and both Python snippets
|
|
51
|
+
in it are executed as part of the checks.
|
|
52
|
+
- The repository is now `haeganm/walkforward`, matching the Python package.
|
|
53
|
+
GitHub redirects the old address and the earlier releases are unaffected.
|
|
54
|
+
The C keeps its `mlr_` prefix and its `include/mlrisk/` headers, the way
|
|
55
|
+
Pillow ships `PIL`.
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
## 3.3.1 (2026-09-04)
|
|
59
|
+
|
|
60
|
+
- `mlr_drawdown_scale` is contemporaneous (`scale[t]` uses the close at the
|
|
61
|
+
end of period `t`) and its header said "multiply position sizes by
|
|
62
|
+
scale_out" with no lag, in the same file whose timing contract says index
|
|
63
|
+
`t` must not know period `t`. Applying `scale[t]` to the position held
|
|
64
|
+
over period `t` de-levers on the bar of a loss using that bar's own close.
|
|
65
|
+
The header, the sizing contract and the README now say to use
|
|
66
|
+
`scale[t-1]`; a test pins the alignment. No code change.
|
|
67
|
+
- The rolling mean and standard deviation shifted every value by the first
|
|
68
|
+
finite value of the whole series and never revisited that choice. A bad
|
|
69
|
+
first tick therefore corrupted every later window: with prices near 100
|
|
70
|
+
and a first value of 1e9, windows that did not even contain it had a
|
|
71
|
+
standard deviation 600% off and a mean off by 1e-6; a first value of 1e12
|
|
72
|
+
made the std meaningless. A long trend away from the starting level eroded
|
|
73
|
+
precision the same way. The offset now comes from inside the current
|
|
74
|
+
window, the accumulators are rebuilt every `window` samples (amortized
|
|
75
|
+
O(1)) and whenever a leaving sample carried almost all of the variance or
|
|
76
|
+
the sum, so an outlier that has left leaves no rounding behind. Measured
|
|
77
|
+
against exact rational arithmetic: 6e-16 relative for the std and an exact
|
|
78
|
+
mean after first ticks of 1e9, 1e12 and 1e15 and along a trend from 100
|
|
79
|
+
to 1e6. Cost: about 6 ns per element for the mean and 20 for the std,
|
|
80
|
+
up from 4 and 15.
|
|
81
|
+
- `mlr_linreg_fit` centered each feature with a plain running sum, so a
|
|
82
|
+
feature at a large level carried rounding of order n * eps * level into
|
|
83
|
+
every centered value: with a price column near 1e9 and unit variation
|
|
84
|
+
the slope was off by 4e-12, and at 1e12 by 7e-7, while an SVD solve
|
|
85
|
+
reached 1e-15. Columns are now shifted by the first row before centering
|
|
86
|
+
(differences of nearby doubles are exact), which puts the slope at 4e-15
|
|
87
|
+
from 1e6 to 1e15, ahead of scikit-learn's 1.7e-5 at 1e15.
|
|
88
|
+
- The rolling mean returned `Inf` and the rolling std returned `0` with
|
|
89
|
+
`MLR_OK` for a window of finite values whose differences overflow (values
|
|
90
|
+
of opposite sign near 1e308): the shifted accumulators overflowed and the
|
|
91
|
+
`fmax` that guards an epsilon-negative variance turned the resulting NaN
|
|
92
|
+
into a zero standard deviation. Such windows are now NaN, the state is
|
|
93
|
+
rebuilt on the next step, and no output is ever Inf.
|
|
94
|
+
- `mlr_garch_fit` documented `MLR_EDOMAIN` for a denormally small variance
|
|
95
|
+
but only rejected zero; at a backcast of 4e-321 it returned `MLR_OK` with
|
|
96
|
+
alpha drifted by 0.003 and `converged = 0`. Variances below `DBL_MIN` are
|
|
97
|
+
now refused as documented; anything above fits to the same parameters as
|
|
98
|
+
the unscaled series.
|
|
99
|
+
- `mlr_kelly_fraction` returned `f = 0` with `MLR_OK` when the squared
|
|
100
|
+
deviations overflowed (variance `Inf`), a silently rounded estimate where
|
|
101
|
+
the same function already refuses the NaN form of the overflow. It now
|
|
102
|
+
returns `MLR_EDOMAIN`.
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
## 3.3.0 (2026-09-04)
|
|
106
|
+
|
|
107
|
+
The last pass before the C API is frozen for language bindings. Three
|
|
108
|
+
reviewers who had not seen the code read it; nine of their findings
|
|
109
|
+
reproduced and are fixed here.
|
|
110
|
+
|
|
111
|
+
### Fixed
|
|
112
|
+
|
|
113
|
+
- The GARCH recursion was written three ways: the likelihood and the fit's
|
|
114
|
+
`sigma2_next` loop as `omega + alpha*r*r + beta*s2`, the filter as
|
|
115
|
+
`omega + alpha*(r*r) + beta*s2`. The two differ by an ulp on about a third
|
|
116
|
+
of steps, so for 12% of fit samples `sigma2_next` was not the value the
|
|
117
|
+
filter reached and the documented bit-exact `mlr_garch_filter_from`
|
|
118
|
+
continuation failed by an ulp. One `garch_step` now serves all three;
|
|
119
|
+
the continuation is tested across 60 fits.
|
|
120
|
+
- `mlr_garch_forecast` accepted `sigma2_next == 0` (the value in a
|
|
121
|
+
hand-built model that never set it) and forecast zero volatility with
|
|
122
|
+
`MLR_OK`. Now `MLR_EINVAL`, matching `mlr_garch_filter_from`.
|
|
123
|
+
- `mlr_linreg_fit` wrote weights into the model during back-substitution
|
|
124
|
+
and the intercept before checking it, so a failing fit left a half-new
|
|
125
|
+
model behind. It now solves into scratch and touches the model only on
|
|
126
|
+
success; the header says "on any failure the model is left exactly as it
|
|
127
|
+
was".
|
|
128
|
+
- `mlr_linreg_predict` on a model that was initialized but never fitted
|
|
129
|
+
returned all zeros with `MLR_OK`. `mlr_lin_model` gained a `fitted` field
|
|
130
|
+
(set by a successful fit, cleared by init and free) and predict requires
|
|
131
|
+
it. Recompile consumers: the struct grew.
|
|
132
|
+
- The installed `mlrisk.pc` located the prefix as `${pcfiledir}/../..`,
|
|
133
|
+
which is wrong when `CMAKE_INSTALL_LIBDIR` is two levels deep
|
|
134
|
+
(`lib/x86_64-linux-gnu`, the Debian and Ubuntu default under `/usr`).
|
|
135
|
+
The relative path is now computed at configure time.
|
|
136
|
+
- `mlr_walk_forward_splits` did not write `*count_out` on its `MLR_EINVAL`
|
|
137
|
+
paths; it now writes 0.
|
|
138
|
+
- The fuzz sweep never fitted a GARCH model: every element was non-finite
|
|
139
|
+
with probability 0.11 and the fit needs 100 finite returns, so the
|
|
140
|
+
success path had probability 9e-6 per round. Every fourth round is now
|
|
141
|
+
clean, split parameters can hit every status code, and the sweep asserts
|
|
142
|
+
that the success paths were reached.
|
|
143
|
+
- The C11 language requirement was exported to consumers through
|
|
144
|
+
`target_compile_features(PUBLIC)`; the public headers need only C99, so
|
|
145
|
+
it is now private. MSVC builds add `/fp:contract-`, the real counterpart
|
|
146
|
+
of `-ffp-contract=off`. The 32-bit CI leg uses SSE math instead of x87.
|
|
147
|
+
- Documentation that no longer matched the code: `linreg.h` described the
|
|
148
|
+
pre-3.2.0 normal-equation solver; `sizing.h`'s lag-before-sizing warning
|
|
149
|
+
omitted the rolling statistics, which are contemporaneous; "with
|
|
150
|
+
ridge > 0 the system is always solvable" was overstated; `mlr_linreg_fit`
|
|
151
|
+
called its in/out model `model_out`.
|
|
152
|
+
|
|
153
|
+
### Added
|
|
154
|
+
|
|
155
|
+
- `mlr_version()` and `mlr_version_number()`, so a binding that loads the
|
|
156
|
+
compiled library can check what it loaded (the reference suite now does).
|
|
157
|
+
- `MLR_GARCH_MIN_N` and `MLR_GARCH_MAX_PERSISTENCE` as public constants.
|
|
158
|
+
- Header contracts a binding author asked for: partially written output on
|
|
159
|
+
`MLR_EDOMAIN`, count-query mode keyed on the pointer, the `backcast == 0`
|
|
160
|
+
sentinel, `converged` exactly 0 or 1, the model owning `w` and not being
|
|
161
|
+
copyable, `mlr_ewma_vol` all-NaN when no usable return precedes the last
|
|
162
|
+
index, the lag warning on each range estimator. README Conventions now
|
|
163
|
+
tabulates the five non-finite-element policies and states the `n == 0`
|
|
164
|
+
and trusted-`size_t` rules.
|
|
165
|
+
- `MLRISK_BUILD_BENCH` option (default off); the benchmark is no longer
|
|
166
|
+
built with the examples.
|
|
167
|
+
- A reference check that the purge rule is exactly the leakage boundary:
|
|
168
|
+
with labels spanning h periods, purge = h-1 leaves no training label
|
|
169
|
+
inside the test window and purge = h-2 does, for h = 2, 5 and 21.
|
|
170
|
+
- The README says to demean with the training-window mean before fitting
|
|
171
|
+
(the full-sample mean is a lookahead) and quantifies the drift bias.
|
|
172
|
+
|
|
173
|
+
### Changed
|
|
174
|
+
|
|
175
|
+
- Size-overflow rejections in `mlr_lin_model_init` and `mlr_linreg_fit`
|
|
176
|
+
return `MLR_EINVAL` (a dimension that cannot be sized is bad input);
|
|
177
|
+
`MLR_ENOMEM` is reserved for a real allocation failure.
|
|
178
|
+
- `tests/reference/requirements.txt` pins `statsmodels`; the reference
|
|
179
|
+
build uses `-O3` to match the CMake Release build.
|
|
180
|
+
- Work arrays in `mlr_linreg_fit` are zero-initialized; GCC's analyzer
|
|
181
|
+
flagged reads it could not prove written (they were), and removing the
|
|
182
|
+
question costs nothing next to the solve.
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
## 3.2.0 (2026-09-04)
|
|
186
|
+
|
|
187
|
+
A second review (by a reader who had not seen the code before) named six
|
|
188
|
+
weak spots. Four were real bugs or traps and are fixed; the other two were
|
|
189
|
+
design choices and are now either better or better documented.
|
|
190
|
+
|
|
191
|
+
### Fixed
|
|
192
|
+
|
|
193
|
+
- `mlr_linreg_fit` sized its work arrays with a constant chosen for 64-bit
|
|
194
|
+
`size_t`; on 32-bit targets the `d * (d + 1)` product wrapped for d above
|
|
195
|
+
about 23,000 and the fit could write past its allocation. Sizes are now
|
|
196
|
+
checked arithmetically before anything is allocated or read.
|
|
197
|
+
- `mlr_vol_target_position` accepted an `equity` and `max_leverage` whose
|
|
198
|
+
product overflowed, which made the cap infinite and therefore never
|
|
199
|
+
applied. Such a pair is now `MLR_EINVAL`.
|
|
200
|
+
- Passing the same array as input and output corrupted rolling and EWMA
|
|
201
|
+
results. Output parameters are now `restrict`-qualified (`MLR_RESTRICT`,
|
|
202
|
+
empty under C++) and the contract is documented in `types.h`.
|
|
203
|
+
- Continuing a fitted GARCH model onto new data by filtering the new data
|
|
204
|
+
alone restarted the recursion from the pre-sample backcast (about 30% off
|
|
205
|
+
at the first period on the test series). `mlr_garch_filter_from` starts
|
|
206
|
+
from a given variance, and with `model->sigma2_next` reproduces the tail
|
|
207
|
+
of the full filter bit for bit; the example uses it and the plain filter's
|
|
208
|
+
header explains the trap.
|
|
209
|
+
|
|
210
|
+
### Changed
|
|
211
|
+
|
|
212
|
+
- The regression solver is Householder QR on the centered design with
|
|
213
|
+
`sqrt(ridge) I` appended, instead of Gaussian elimination on the normal
|
|
214
|
+
equations. Accuracy is now about condition number times epsilon (2.8e-8
|
|
215
|
+
at condition number 1e8, where the old solver refused the problem and was
|
|
216
|
+
already at 1.5e-5 by 1e6). Same API, same results on well-conditioned data
|
|
217
|
+
to 1e-14.
|
|
218
|
+
- `mlr_kelly_fraction` is documented as a sizing utility and an upper bound,
|
|
219
|
+
not an allocation model.
|
|
220
|
+
|
|
221
|
+
### Added
|
|
222
|
+
|
|
223
|
+
- Reference checks for the QR solver on designs with condition numbers
|
|
224
|
+
1e4 to 1e10 against SVD least squares, and for `mlr_garch_filter_from`
|
|
225
|
+
continuation; C tests for the ill-conditioned fit, the cap overflow, the
|
|
226
|
+
continuation, and the 32-bit size guard.
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
## 3.1.0 (2026-09-03)
|
|
230
|
+
|
|
231
|
+
Verification release. Every public function is now compared against an
|
|
232
|
+
independent implementation on every push, and the pass turned up a handful
|
|
233
|
+
of contract gaps.
|
|
234
|
+
|
|
235
|
+
### Fixed
|
|
236
|
+
|
|
237
|
+
- The GARCH optimizer's convergence test measured the simplex relative to
|
|
238
|
+
each parameter's own magnitude, so a maximum on the boundary `alpha = 0`
|
|
239
|
+
(returns with no ARCH effect) could never satisfy it: the fit ran to the
|
|
240
|
+
2000-iteration cap, took 16x longer than it should, and reported
|
|
241
|
+
`converged = 0` on a finished fit. The simplex is now measured against a
|
|
242
|
+
fixed scale per parameter (the backcast for omega, 1 for alpha and beta).
|
|
243
|
+
- `mlr_garch_fit` ran Nelder-Mead once, from the best grid point, and could
|
|
244
|
+
stop in the wrong basin: against a 108-start brute-force search on the
|
|
245
|
+
identical likelihood it lost on three of eleven adversarial series (tiny
|
|
246
|
+
alpha with a second maximum at persistence 0.99, a 50-sigma outlier, a
|
|
247
|
+
fourfold variance regime switch), by up to 1.7 log-likelihood units. It now
|
|
248
|
+
runs from its three best grid points, restarts each from its own result
|
|
249
|
+
until that stops improving, and keeps the best; it matches the brute-force
|
|
250
|
+
optimum on ten of the eleven and beats it on the outlier series. Fits cost 5 to 7x what they did (8 ms at n = 1000).
|
|
251
|
+
- `mlr_vol_target_position` let a denormal price overflow `equity / price`
|
|
252
|
+
past the leverage cap (an infinite position with `MLR_OK`), and let a
|
|
253
|
+
denormal position slip the cap by rounding; both are now zero.
|
|
254
|
+
- `mlr_garch_filter` and `mlr_garch_forecast` emitted `Inf` when extreme but
|
|
255
|
+
valid parameters overflowed the recursion; they now return `MLR_EDOMAIN`.
|
|
256
|
+
- `mlr_lin_model_init` and `mlr_linreg_fit` refuse dimensions whose
|
|
257
|
+
allocation size would overflow (`MLR_ENOMEM`) instead of relying on
|
|
258
|
+
`calloc` to notice.
|
|
259
|
+
- `mlr_garch_filter` treated a finite return whose square overflows as data,
|
|
260
|
+
pinning every later sigma at `Inf` with `MLR_OK`; it is now treated as
|
|
261
|
+
missing, the same rule EWMA already used.
|
|
262
|
+
- `mlr_garman_klass_vol` accepted bars with the open or close outside
|
|
263
|
+
`[low, high]`. They now give `NAN`. On a consistent bar the estimator is
|
|
264
|
+
bounded below by `0.114 * ln(high/low)^2`, so the "negative variance"
|
|
265
|
+
branch was unreachable and has been removed from the code and the docs.
|
|
266
|
+
- The installed `mlrisk.pc` hard-coded the configure-time prefix, so an
|
|
267
|
+
install with `--prefix` produced a pkg-config file pointing at the wrong
|
|
268
|
+
tree. It is now relocatable (`${pcfiledir}/../..`).
|
|
269
|
+
- `$<INSTALL_INTERFACE>` hard-coded `include` instead of
|
|
270
|
+
`CMAKE_INSTALL_INCLUDEDIR`; install and export rules are now behind
|
|
271
|
+
`MLRISK_INSTALL` so a parent project does not inherit them.
|
|
272
|
+
- A GCC `-Wmaybe-uninitialized` in the GARCH grid search would have failed
|
|
273
|
+
the `-Werror` build on the Linux/gcc CI leg.
|
|
274
|
+
- `mlr_drawdown_scale` returned `MLR_EDOMAIN` for a non-finite equity value
|
|
275
|
+
while every other function returns `MLR_EINVAL` for non-finite input; it
|
|
276
|
+
now returns `MLR_EINVAL` (non-positive equity is still `MLR_EDOMAIN`).
|
|
277
|
+
- Header and README claims corrected: the PnL of a position is
|
|
278
|
+
`position[t] * price[t-1] * returns[t]` (the price factor was missing);
|
|
279
|
+
the GARCH constraint is `alpha + beta < 0.9999`; only alpha and beta are
|
|
280
|
+
scale invariant; `n <= d` with `ridge == 0` returns `MLR_EDOMAIN` rather
|
|
281
|
+
than fitting exactly; rolling std is O(n) on clean data with an O(window)
|
|
282
|
+
rebuild after each gap.
|
|
283
|
+
|
|
284
|
+
### Added
|
|
285
|
+
|
|
286
|
+
- `tests/reference/reference_check.py`: 19 checks of the compiled C against
|
|
287
|
+
pandas, numpy, scikit-learn, `arch`, exact rational arithmetic, an
|
|
288
|
+
independent split generator, a bitwise no-lookahead sweep, a 400-fit
|
|
289
|
+
GARCH Monte Carlo, and a reconciliation of the example's printed PnL.
|
|
290
|
+
A `reference` CI job runs it on every push.
|
|
291
|
+
- Prefix-stability tests for EWMA and the rolling statistics (previously
|
|
292
|
+
only the GARCH filter had one), tests for `window == n`, all-NaN rolling
|
|
293
|
+
input, NULL outputs on the range estimators, and the GARCH filter
|
|
294
|
+
overflow rule.
|
|
295
|
+
- `tests/test_fuzz.c`: 4000 randomized calls across the whole API (NaN, Inf,
|
|
296
|
+
denormals, `SIZE_MAX` arguments) asserting the documented contracts; runs
|
|
297
|
+
under the sanitizers in CI and found the two sizing/filter holes above.
|
|
298
|
+
- `tests/reference/real_data_check.py` for daily OHLC files, and
|
|
299
|
+
`bench/bench.c` (`mlrisk_bench`) with per-element timings and scaling ratios.
|
|
300
|
+
- A 32-bit (`-m32`) CI leg, so the `size_t` guards are exercised where
|
|
301
|
+
`size_t` is 32 bits; the workflow now runs on every branch push and can be
|
|
302
|
+
dispatched by hand.
|
|
303
|
+
- The sanitizer CI job also runs the example; the Windows example step
|
|
304
|
+
declares its shell; the workflow passes `actionlint`.
|
|
305
|
+
- The C two-pass reference in the rolling tests is computed on shifted
|
|
306
|
+
values so that it is itself exact at large levels.
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
## 3.0.0 (2026-09-03)
|
|
310
|
+
|
|
311
|
+
Breaking release. Every volatility forecast now shares one timing convention,
|
|
312
|
+
and a technical audit closed a set of silent-failure paths.
|
|
313
|
+
|
|
314
|
+
### Breaking
|
|
315
|
+
|
|
316
|
+
- `mlr_ewma_vol` is predictive: `out[t]` is the forecast for period t from
|
|
317
|
+
`returns[0..t-1]`, matching `mlr_garch_filter`. Previously `out[t]` included
|
|
318
|
+
`returns[t]`, so sizing period t from it was a one-bar lookahead. `out[0]` is
|
|
319
|
+
now `NAN`. `lambda` must be in `[0, 1)`.
|
|
320
|
+
- `mlr_lin_model_init(model, d)` drops the ridge argument; the `ridge` field is
|
|
321
|
+
written by `mlr_linreg_fit` with the value actually used (it was previously
|
|
322
|
+
never updated and could disagree with the fit).
|
|
323
|
+
- `mlr_garch_fit` requires `n >= 100` (was 20).
|
|
324
|
+
- `mlr_garch` gained a `backcast` field (appended). The recursion starts from
|
|
325
|
+
`sigma2[0] = omega + (alpha + beta) * backcast`, the `arch` presample rule.
|
|
326
|
+
Fitted parameters move by about 1e-5 relative to 2.0.0.
|
|
327
|
+
- CMake 3.21 is required; the library compiles as strict ISO C11.
|
|
328
|
+
|
|
329
|
+
### Fixed
|
|
330
|
+
|
|
331
|
+
- `mlr_garch_filter` seeded its recursion from the series being filtered, so
|
|
332
|
+
every output depended on future returns. The seed now comes from the model.
|
|
333
|
+
- `mlr_garch_fit` had an absolute feasibility floor on `omega`; returns with
|
|
334
|
+
rms below about 2e-8 returned an unfitted model with `MLR_OK`. The bound is
|
|
335
|
+
now positivity only and estimates are scale invariant.
|
|
336
|
+
- Nelder-Mead stopped on function-value spread alone, so parameter accuracy
|
|
337
|
+
varied about 1000x with the units of the returns. Convergence now also
|
|
338
|
+
requires a small simplex; accuracy is about 1e-7 at any scale.
|
|
339
|
+
- `mlr_garch_filter` and `mlr_garch_forecast` accepted `omega = Inf` from
|
|
340
|
+
hand-built models and produced `Inf`/`NaN` output with `MLR_OK`.
|
|
341
|
+
- `mlr_walk_forward_splits` could wrap `size_t` arithmetic: a huge `embargo`
|
|
342
|
+
placed the post-test training segment inside the test window, and a huge
|
|
343
|
+
`train_len` produced out-of-range splits, both with `MLR_OK`.
|
|
344
|
+
- `mlr_linreg_fit` accepted NaN or Inf inputs (NaN pivots passed the
|
|
345
|
+
singularity test) and negative ridge, producing garbage models with
|
|
346
|
+
`MLR_OK`. Its singularity threshold had an absolute floor that rejected
|
|
347
|
+
well-conditioned systems with features below about 1e-8.
|
|
348
|
+
- `mlr_linreg_predict` dereferenced a freed model.
|
|
349
|
+
- `mlr_vol_target_position` did not check `equity` or `max_leverage` for
|
|
350
|
+
finiteness: NaN equity gave NaN positions, Inf leverage disabled the cap.
|
|
351
|
+
- `mlr_kelly_fraction` could return NaN with `MLR_OK` on overflowing sums.
|
|
352
|
+
- Missing data no longer poisons `mlr_ewma_vol` or `mlr_garch_filter`; the
|
|
353
|
+
forecast already made is kept and the recursion carries on.
|
|
354
|
+
- Range estimators return `NAN` rather than `Inf` when the price ratio overflows.
|
|
355
|
+
|
|
356
|
+
### Added
|
|
357
|
+
|
|
358
|
+
- `mlr_garch_fit` is checked against the Python `arch` package on identical
|
|
359
|
+
samples with an identical likelihood (`tests/reference/garch_arch_reference.py`).
|
|
360
|
+
- Tests for prefix stability of the GARCH filter, scale invariance of the fit,
|
|
361
|
+
the timing alignment of every forecast, ridge shrinkage, and all of the
|
|
362
|
+
input-validation paths above. Tests run per module under ctest.
|
|
363
|
+
- `mlrisk/version.h` (`MLRISK_VERSION`), a pkg-config file, and CMake options
|
|
364
|
+
`MLRISK_WERROR`, `MLRISK_BUILD_TESTS`, `MLRISK_BUILD_EXAMPLES` for consumers.
|
|
365
|
+
- The example runs a walk-forward GARCH fit, filter, and sizing loop and
|
|
366
|
+
reports realized strategy volatility against the target.
|
|
367
|
+
|
|
368
|
+
## 2.0.0
|
|
369
|
+
|
|
370
|
+
Public API rework: `mlr_` prefix on every function, purged and embargoed
|
|
371
|
+
walk-forward splits with a capacity-safe API, GARCH(1,1), range estimators,
|
|
372
|
+
Kelly and drawdown sizing. No compatibility layer with 1.x.
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
cmake_minimum_required(VERSION 3.21)
|
|
2
|
+
project(mlrisk VERSION 3.4.0 LANGUAGES C)
|
|
3
|
+
|
|
4
|
+
set(CMAKE_C_STANDARD 11)
|
|
5
|
+
set(CMAKE_C_STANDARD_REQUIRED ON)
|
|
6
|
+
set(CMAKE_C_EXTENSIONS OFF)
|
|
7
|
+
|
|
8
|
+
option(MLRISK_WERROR "Treat warnings as errors" ${PROJECT_IS_TOP_LEVEL})
|
|
9
|
+
option(MLRISK_BUILD_TESTS "Build the test suite" ${PROJECT_IS_TOP_LEVEL})
|
|
10
|
+
option(MLRISK_BUILD_EXAMPLES "Build the examples" ${PROJECT_IS_TOP_LEVEL})
|
|
11
|
+
option(MLRISK_BUILD_BENCH "Build the benchmark" OFF)
|
|
12
|
+
option(MLRISK_INSTALL "Generate install and export rules" ${PROJECT_IS_TOP_LEVEL})
|
|
13
|
+
|
|
14
|
+
include(GNUInstallDirs)
|
|
15
|
+
|
|
16
|
+
if(MSVC)
|
|
17
|
+
# /fp:contract- forbids fused multiply-add the way -ffp-contract=off does
|
|
18
|
+
set(MLRISK_COMPILE_OPTIONS /W4 /fp:precise /fp:contract- $<$<BOOL:${MLRISK_WERROR}>:/WX>)
|
|
19
|
+
else()
|
|
20
|
+
# -ffp-contract=off: no fused multiply-add, so results match across
|
|
21
|
+
# compilers and architectures to the last bit
|
|
22
|
+
set(MLRISK_COMPILE_OPTIONS -Wall -Wextra -Wpedantic -ffp-contract=off
|
|
23
|
+
$<$<BOOL:${MLRISK_WERROR}>:-Werror>)
|
|
24
|
+
endif()
|
|
25
|
+
|
|
26
|
+
configure_file(include/mlrisk/version.h.in include/mlrisk/version.h @ONLY)
|
|
27
|
+
|
|
28
|
+
add_library(mlrisk STATIC
|
|
29
|
+
src/rolling.c
|
|
30
|
+
src/sizing.c
|
|
31
|
+
src/vol.c
|
|
32
|
+
src/split.c
|
|
33
|
+
src/linreg.c
|
|
34
|
+
src/version.c
|
|
35
|
+
)
|
|
36
|
+
add_library(mlrisk::mlrisk ALIAS mlrisk)
|
|
37
|
+
|
|
38
|
+
# Built as C11; the public headers need only C99, so no standard is exported
|
|
39
|
+
target_compile_features(mlrisk PRIVATE c_std_11)
|
|
40
|
+
target_compile_options(mlrisk PRIVATE ${MLRISK_COMPILE_OPTIONS})
|
|
41
|
+
target_include_directories(mlrisk PUBLIC
|
|
42
|
+
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
|
|
43
|
+
$<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}/include>
|
|
44
|
+
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
|
|
45
|
+
)
|
|
46
|
+
if(UNIX)
|
|
47
|
+
target_link_libraries(mlrisk PUBLIC m)
|
|
48
|
+
endif()
|
|
49
|
+
|
|
50
|
+
if(MLRISK_BUILD_TESTS)
|
|
51
|
+
enable_testing()
|
|
52
|
+
add_executable(mlrisk_tests
|
|
53
|
+
tests/test_main.c
|
|
54
|
+
tests/test_rolling.c
|
|
55
|
+
tests/test_sizing.c
|
|
56
|
+
tests/test_vol.c
|
|
57
|
+
tests/test_split.c
|
|
58
|
+
tests/test_linreg.c
|
|
59
|
+
tests/test_fuzz.c
|
|
60
|
+
)
|
|
61
|
+
target_compile_options(mlrisk_tests PRIVATE ${MLRISK_COMPILE_OPTIONS})
|
|
62
|
+
target_link_libraries(mlrisk_tests PRIVATE mlrisk::mlrisk)
|
|
63
|
+
foreach(module rolling vol sizing split linreg fuzz)
|
|
64
|
+
add_test(NAME ${module} COMMAND mlrisk_tests ${module})
|
|
65
|
+
endforeach()
|
|
66
|
+
endif()
|
|
67
|
+
|
|
68
|
+
if(MLRISK_BUILD_EXAMPLES)
|
|
69
|
+
add_executable(vol_target_demo examples/vol_target_demo.c)
|
|
70
|
+
target_compile_options(vol_target_demo PRIVATE ${MLRISK_COMPILE_OPTIONS})
|
|
71
|
+
target_link_libraries(vol_target_demo PRIVATE mlrisk::mlrisk)
|
|
72
|
+
|
|
73
|
+
endif()
|
|
74
|
+
|
|
75
|
+
if(MLRISK_BUILD_BENCH)
|
|
76
|
+
add_executable(mlrisk_bench bench/bench.c)
|
|
77
|
+
target_compile_options(mlrisk_bench PRIVATE ${MLRISK_COMPILE_OPTIONS})
|
|
78
|
+
target_link_libraries(mlrisk_bench PRIVATE mlrisk::mlrisk)
|
|
79
|
+
endif()
|
|
80
|
+
|
|
81
|
+
if(MLRISK_INSTALL)
|
|
82
|
+
include(CMakePackageConfigHelpers)
|
|
83
|
+
|
|
84
|
+
install(TARGETS mlrisk EXPORT mlriskTargets
|
|
85
|
+
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR})
|
|
86
|
+
install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
|
|
87
|
+
FILES_MATCHING PATTERN "*.h")
|
|
88
|
+
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/include/mlrisk/version.h
|
|
89
|
+
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/mlrisk)
|
|
90
|
+
install(EXPORT mlriskTargets NAMESPACE mlrisk::
|
|
91
|
+
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/mlrisk)
|
|
92
|
+
|
|
93
|
+
configure_package_config_file(cmake/mlriskConfig.cmake.in
|
|
94
|
+
${CMAKE_CURRENT_BINARY_DIR}/mlriskConfig.cmake
|
|
95
|
+
INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/mlrisk)
|
|
96
|
+
write_basic_package_version_file(
|
|
97
|
+
${CMAKE_CURRENT_BINARY_DIR}/mlriskConfigVersion.cmake
|
|
98
|
+
COMPATIBILITY SameMajorVersion)
|
|
99
|
+
install(FILES
|
|
100
|
+
${CMAKE_CURRENT_BINARY_DIR}/mlriskConfig.cmake
|
|
101
|
+
${CMAKE_CURRENT_BINARY_DIR}/mlriskConfigVersion.cmake
|
|
102
|
+
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/mlrisk)
|
|
103
|
+
|
|
104
|
+
# The .pc file locates the prefix relative to its own directory, whatever
|
|
105
|
+
# the depth of CMAKE_INSTALL_LIBDIR (lib, lib64, lib/x86_64-linux-gnu, ...)
|
|
106
|
+
file(RELATIVE_PATH MLRISK_PC_TO_PREFIX
|
|
107
|
+
"${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}/pkgconfig" "${CMAKE_INSTALL_PREFIX}")
|
|
108
|
+
string(REGEX REPLACE "/$" "" MLRISK_PC_TO_PREFIX "${MLRISK_PC_TO_PREFIX}")
|
|
109
|
+
configure_file(cmake/mlrisk.pc.in ${CMAKE_CURRENT_BINARY_DIR}/mlrisk.pc @ONLY)
|
|
110
|
+
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/mlrisk.pc
|
|
111
|
+
DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig)
|
|
112
|
+
endif()
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 mlrisk contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: walkforward
|
|
3
|
+
Version: 3.4.0
|
|
4
|
+
Summary: Volatility forecasts, position sizing and purged walk-forward splits that never see the future
|
|
5
|
+
Keywords: walk-forward,purged-cross-validation,backtesting,lookahead-bias,volatility,garch,position-sizing,quantitative-finance
|
|
6
|
+
Author: Haegan McGarry
|
|
7
|
+
License: MIT
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: C
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
16
|
+
Project-URL: Homepage, https://github.com/haeganm/walkforward
|
|
17
|
+
Project-URL: Changelog, https://github.com/haeganm/walkforward/blob/main/CHANGELOG.md
|
|
18
|
+
Requires-Python: >=3.9
|
|
19
|
+
Requires-Dist: numpy>=1.23
|
|
20
|
+
Provides-Extra: sklearn
|
|
21
|
+
Requires-Dist: scikit-learn>=1.1; extra == "sklearn"
|
|
22
|
+
Provides-Extra: test
|
|
23
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
24
|
+
Requires-Dist: scikit-learn>=1.1; extra == "test"
|
|
25
|
+
Requires-Dist: pandas>=1.5; extra == "test"
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# walkforward
|
|
29
|
+
|
|
30
|
+
> Volatility forecasts, position sizing and purged walk-forward splits that never see the future. A C11 library with a numpy front door.
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install walkforward
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
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.
|
|
37
|
+
|
|
38
|
+
- **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.
|
|
39
|
+
- **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`.
|
|
40
|
+
- **GARCH(1,1) by maximum likelihood**, checked against `arch` on identical samples and identical likelihoods, and invariant to the units of the returns.
|
|
41
|
+
- **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.
|
|
42
|
+
- **Ridge by Householder QR**, so a feature at a price level near 1e9 keeps its slope precision.
|
|
43
|
+
|
|
44
|
+
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.
|
|
45
|
+
|
|
46
|
+
## The loop
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
import numpy as np
|
|
50
|
+
import walkforward as wf
|
|
51
|
+
|
|
52
|
+
# The fit assumes mean-zero returns. Demean with the TRAINING mean; the
|
|
53
|
+
# full-sample mean would put the future into the fit.
|
|
54
|
+
train = returns[:1000] - returns[:1000].mean()
|
|
55
|
+
model = wf.garch_fit(train)
|
|
56
|
+
|
|
57
|
+
# sigma[t] forecasts period t from returns before t. filter_from continues
|
|
58
|
+
# from the variance state the fit ended on, which is what makes the
|
|
59
|
+
# out-of-sample path the same as filtering everything together.
|
|
60
|
+
sigma = model.filter_from(returns[1000:])
|
|
61
|
+
|
|
62
|
+
# A position held over period t is entered at the close of t-1, so it is
|
|
63
|
+
# sized against the previous close, and earns position * price * return.
|
|
64
|
+
entry = close[999:-1]
|
|
65
|
+
position = wf.vol_target_position(
|
|
66
|
+
sigma, target_vol=0.01, equity=100_000.0, price=entry, max_leverage=2.0
|
|
67
|
+
)
|
|
68
|
+
pnl = position * entry * returns[1000:]
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`model.persistence`, `model.half_life` and `model.unconditional_vol` are there so you do not have to recompute them.
|
|
72
|
+
|
|
73
|
+
## Cross-validation
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from sklearn.ensemble import GradientBoostingRegressor
|
|
77
|
+
from sklearn.model_selection import cross_val_score
|
|
78
|
+
import walkforward as wf
|
|
79
|
+
|
|
80
|
+
# Target is a 21-period forward return, so labels span 21 periods and the
|
|
81
|
+
# last 20 training rows before each test window are dropped.
|
|
82
|
+
cv = wf.PurgedWalkForward(train_size=756, test_size=252, label_horizon=21)
|
|
83
|
+
|
|
84
|
+
scores = cross_val_score(GradientBoostingRegressor(), X, y, cv=cv)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
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.
|
|
88
|
+
|
|
89
|
+
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.
|
|
90
|
+
|
|
91
|
+
## Conventions
|
|
92
|
+
|
|
93
|
+
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.
|
|
94
|
+
|
|
95
|
+
`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))`.
|
|
96
|
+
|
|
97
|
+
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.
|
|
98
|
+
|
|
99
|
+
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.
|
|
100
|
+
|
|
101
|
+
## What it is checked against
|
|
102
|
+
|
|
103
|
+
The C library underneath is compared on every push to independent implementations, and the numbers are reproducible from `tests/reference/` in the repository.
|
|
104
|
+
|
|
105
|
+
| Check | Reference | Result |
|
|
106
|
+
|---|---|---|
|
|
107
|
+
| Rolling mean and std, with gaps | pandas | 3.1e-11 |
|
|
108
|
+
| 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 |
|
|
109
|
+
| EWMA, predictive alignment | pandas `ewm` shifted one period | 3.5e-18 |
|
|
110
|
+
| GARCH filter and forecast | `arch` | 4.4e-16 relative |
|
|
111
|
+
| GARCH fit over 20 samples | `arch`, same likelihood | 5.0e-7 max parameter difference |
|
|
112
|
+
| Ridge, condition number 1e4 to 1e10 | SVD least squares | within 2x condition times epsilon |
|
|
113
|
+
| Ridge with a feature at level 1e6 to 1e15 | exact rational OLS | under 1e-14 |
|
|
114
|
+
| Walk-forward splits, 1620 parameter sets | independent generator | 0 mismatches |
|
|
115
|
+
| No lookahead, 40 trials | bitwise prefix comparison | 0 violations |
|
|
116
|
+
|
|
117
|
+
The C also runs under AddressSanitizer and UndefinedBehaviorSanitizer, on 32-bit and 64-bit, and gives bit-identical answers under GCC and Clang.
|
|
118
|
+
|
|
119
|
+
## License
|
|
120
|
+
|
|
121
|
+
MIT. Source and the C library: https://github.com/haeganm/walkforward
|