GPU-accelerated walk-forward optimization for evaluating systematic trading strategies.
wfo-gpu measures whether a family of trading strategies retains its performance when it is periodically re-optimized and then traded on data it has not seen. It backtests hundreds of thousands of strategy configurations on an OpenCL device or a multithreaded C++ engine with matching results, filters them for robustness and cost viability, and evaluates the survivors with walk-forward re-optimization across 434 schedule and selection-rule combinations.
Out-of-sample results of every walk-forward run on two synthetic datasets. None of the runs on a random walk (blue) is profitable; on a series with a planted trend (orange), the edge is identified and persists out of sample. Produced by the quick start below; details in Example results.
| Scope | Two-stage research pipeline: a parameter screen over a combinatorial strategy grid, followed by walk-forward re-optimization with out-of-sample evaluation |
| Validity controls | Lookahead prevented by construction and checked by perturbation tests; spread and slippage charged on every trade; null-hypothesis runs on synthetic data |
| Implementation | C++20 and CMake; an OpenCL backtest kernel and a native C++ engine held to the same results; about 500 GoogleTest tests; CI on Linux and Windows |
| Platforms | Any OpenCL GPU or CPU device, or none; Windows and Linux are tested in CI, and the code compiles for macOS |
Project status. This is a personal research project. It has a substantial test suite but has not undergone independent review or production hardening. Its output should be treated as research, not as trading advice.
- How it works
- Quick start
- Example results
- Engineering highlights
- Using your own data
- Documentation
- Limitations
- A note on results
- Development approach
A strategy optimized and evaluated on the same history overstates its performance: given enough candidate configurations, some will fit the past by chance. Walk-forward analysis separates the two. Parameters are selected on an in-sample window and evaluated on the out-of-sample window that follows, and the process repeats across the data, so every reported result comes from bars the selection did not use.
flowchart LR
A[Price bars] --> B[Screen:<br/>backtest the<br/>strategy grid]
B --> C[Filter for robustness<br/>and cost viability]
C --> D[Walk-forward:<br/>select on each<br/>in-sample window]
D --> E[Evaluate on the<br/>following<br/>out-of-sample window]
E --> F[Stitched<br/>out-of-sample<br/>results]
- Screen. Each strategy configuration is backtested over an initial range of the data. A configuration combines up to four of 643 signals (technical indicators and session-structure features) with weights, an aggregation rule, an entry threshold, one of 24 exit rules and a trading-hours filter. To advance, a configuration must clear sanity floors, earn an average net trade of at least twice its round-trip cost, and retain its consistency when any single parameter moves one grid step. The Pareto frontier of the survivors and its robust neighbors form the reduced set.
- Walk-forward. The reduced set is evaluated under 62 schedules, each defined by a re-optimization interval, an in-sample lookback and an optional drawdown or losing-streak trigger. In each in-sample window, seven selection rules each choose one configuration, which then trades the following out-of-sample window at an entry delay of one bar and, as a robustness check, two bars. The out-of-sample segments are stitched into one result per schedule and selection rule.
Requirements: CMake 3.20 or later and a C++20 compiler (GCC 11+, Clang 14+ or MSVC 2022). An OpenCL SDK is optional; without one, all computation runs on the native C++ path. Third-party dependencies are fetched by CMake.
git clone https://github.com/triwebb1/wfo-gpu.git
cd wfo-gpu
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --config Release --parallel
ctest --test-dir build --build-config ReleaseThe following runs the full pipeline on one year of synthetic 30-second bars, first a random walk and then a series with hidden trend regimes:
./build/wfo-gpu generate --out data/random_walk.bin --days 260
./build/wfo-gpu run --config config/example.toml --bars data/random_walk.bin
./build/wfo-gpu generate --out data/trending.bin --days 260 --trend 0.03
./build/wfo-gpu run --config config/example.toml --bars data/trending.bin --output output/trendingEach run screens 308,640 configurations and evaluates 434 walk-forward combinations, which takes about seven minutes on eight CPU threads. With the Visual Studio generator on Windows, the executables are in build\Release\.
Results of the two quick-start runs with config/example.toml. The screen uses approximately the first 22 sessions; walk-forward evaluation trades only the bars after them. All figures come from synthetic data.
| Random walk (no edge) | Planted trend (known edge) | |
|---|---|---|
| Configurations screened | 308,640 | 308,640 |
| Passed the sanity floors | 18,828 | 23,504 |
| Retained for walk-forward | 19 | 57 |
| Walk-forward runs with at least 4 windows | 105 | 196 |
| Median out-of-sample annual return on capital | -77.1% | +449.6% |
| Profitable out-of-sample runs | 0 of 105 | 188 of 196 |
On the random walk, thousands of configurations pass the screen by chance, and none of the resulting walk-forward runs is profitable out of sample. On the trending series, the pipeline identifies the planted edge and nearly all runs remain profitable. A sound optimizer must exhibit both behaviors; the methodology describes the check.
Command-line output of both runs (abridged)
Random walk:
Screen
strategies evaluated 308640
passed sanity floors 18828
economically viable 3865
on Pareto frontier 7
kept for walk-forward 19
Out-of-sample across 105 runs with 4+ windows: median ROC -77.09%, 0 of 105 profitable
Top runs by out-of-sample return on capital
step lookback trigger selection OOS ROC% Sharpe PF MaxDD$ trades windows
30d 30d fixed best_roc -7.12 -0.12 0.98 58398 1060 11
30d 30d fixed best_consistency -30.30 -0.55 0.93 77409 1005 11
14d 30d fixed frontier_centroid -32.97 -0.56 0.93 60848 1132 23
Planted trend:
Screen
strategies evaluated 308640
passed sanity floors 23504
economically viable 10295
on Pareto frontier 17
kept for walk-forward 57
Out-of-sample across 196 runs with 4+ windows: median ROC 449.56%, 188 of 196 profitable
Top runs by out-of-sample return on capital
step lookback trigger selection OOS ROC% Sharpe PF MaxDD$ trades windows
14d 183d fixed best_roc 784.78 4.25 1.50 33993 907 12
14d 183d fixed roc_x_consistency 744.73 4.19 1.49 33993 926 12
14d 91d fixed roc_x_consistency 723.33 5.45 1.65 27268 1476 19
The command-line report presents the distribution before the top-ranked runs. The top-ranked runs are selected after the fact from many correlated results and therefore overstate expected performance.
- Implicit strategy grid. The grid spans more than 4 x 10^15 configurations and is never materialized. Any configuration is reconstructed from its index by combinatorial unranking, neighbors are located by index arithmetic, and configurations that would trade identically are skipped.
- GPU batch evaluation. One work-item per configuration over Structure-of-Arrays bar buffers, a single kernel launch per batch with work-items ordered by signal set and exit rule, pooled device buffers, and kernel sources embedded in the binary at build time.
- Device-independent results. Signals are computed on the host, the kernel is compiled without floating-point contraction, and every result that informs a decision is recomputed by the C++ engine. End-to-end tests require the screen and the walk-forward stage to produce the same results on an OpenCL device as on the native path.
- Lookahead controls. Fills occur strictly after the signal bar, higher-timeframe indicators read only completed bars, session features are withheld until their inputs exist, and in markets with a daily close, positions are closed at each session close. Regression tests perturb all bars after a cut point and require every signal up to that point to remain bit-identical.
- Robust selection. Sanity floors, a cost-based viability floor, a parameter-neighborhood stability score, a five-objective Pareto frontier and seven interchangeable selection rules.
- Fault-tolerant long runs. Atomic, durable file replacement on Windows and POSIX; CRC32C-verified checkpoints bound to the configuration and the input data; stages that resume from their last checkpoint, with tests that interrupt a run twice and compare the result with an uninterrupted run; and a single-instance run lock.
- Independent verification. Kernel results are compared with the C++ engine, indicator values with a separate NumPy implementation, and the command-line pipeline is exercised end to end in CI.
Measured throughput, together with the benchmark workload and the command that reproduces it, is reported in docs/BENCHMARKS.md.
Bars are imported from CSV files with timestamp,open,high,low,close,volume columns. The configurations in config/ cover E-mini equity index futures (es.toml), gold futures (gc.toml) and a 24-hour cryptocurrency market (btc.toml).
./build/wfo-gpu convert --csv my_bars.csv --out data/my_bars.bin --symbol ES
./build/wfo-gpu info --bars data/my_bars.bin
./build/wfo-gpu run --config config/es.toml --bars data/my_bars.binA run writes the following to its [data] output directory:
| File | Contents |
|---|---|
summary.csv |
one row per schedule and selection rule: out-of-sample PnL, return on capital, Sharpe ratio, drawdown |
equity/*.csv |
stitched out-of-sample equity curves (plotted by tools/plot_equity.py) |
details/*.csv |
per-window results: in-sample and out-of-sample metrics, the two-bar-delay re-test, the selected configuration |
landscape_summary.csv |
the screen's filtering funnel and the distributions of surviving configurations |
reduced_params.bin, all_results.bin |
the configurations passed to walk-forward, and every screen result |
meta_analysis.csv |
average out-of-sample performance by selection rule (descriptive, since it is computed on out-of-sample data) |
checkpoints/ |
state for resuming with --resume |
Additional commands:
wfo-gpu info --strategy IDdescribes the rules of any configuration listed in the reports;info --signalslists the signal catalog.wfo-gpu detail --config config/es.toml --bars data/my_bars.bin --ids ids.txtwrites complete trade logs and quarterly PnL for selected configurations.tools/plot_distribution.pyproduces the distribution chart shown above for any set of runs.
The backtest engines accept plain arrays, so any series in [-1, +1] aligned to the bars can serve as a signal. examples/custom_signal evaluates exit rules over a user-defined indicator, and examples/walk_forward_loop implements a walk-forward loop directly on the library interfaces.
| Document | Contents |
|---|---|
| ARCHITECTURE.md | data layer, signals, strategy grid, GPU and CPU backends, selection, reliability and testing |
| METHODOLOGY.md | the backtesting rules the engine enforces, their rationale, and known limitations |
| DATA_FORMAT.md | the binary bar format, CSV import, and reading the format from Python |
| BENCHMARKS.md | measured throughput and reproduction instructions |
- Bar data only: no order book, queue position or partial fills.
- One contract per position, with constant spread and slippage.
- Built-in session models cover US futures trading hours and 24-hour markets.
- The screen enumerates the strategy grid in order: all single-signal configurations first, then pairs as far as the configured budget allows.
- The software is a research tool and does not place orders.
The complete list is in the methodology.
I have run a great many strategies through this pipeline, and none of them held up out of sample over multi-year periods. That is a statement about what I tested and how this tool tests it (systematic signals on bar data, with realistic costs), not about markets in general.
If you find one that holds up, please let me know. I'll wait.
I developed wfo-gpu using spec-driven development. Before implementation, I wrote the specification and the backtesting standards: the two-stage screen and walk-forward design, the lookahead and cost rules (now docs/METHODOLOGY.md), the robustness and selection criteria, the fault-tolerance requirements, and the parity contract between the GPU and CPU engines. I then directed the implementation against that specification with an AI coding agent (Claude), made and reviewed the design decisions, and defined the verification strategy: GPU/CPU parity tests, an independent NumPy cross-check, lookahead perturbation tests and null-hypothesis runs on synthetic data.
That verification identified a real defect during development. A random-walk run that appeared profitable out of sample led to the full-pipeline lookahead test, which traced the cause to session-feature gates that compared clock times across the overnight session boundary.
