Runbook: fracdiff features against returns in a classifier
Notebook: notebooks/python/14_fracdiff_features_classifier.ipynb,
committed with its outputs and executed in CI by just notebooks-run.
Module background: fracdiff, evaluation,
cross_validation. Follows
runbook 09.
Where the question comes from
Section titled “Where the question comes from”Runbook 09 swept the fractional differencing order and decided not to promote “the smallest passing ADF” as a default transform. Issue #49 also asked whether FFD features beat plain returns in a purged-CV classifier, net of costs. On random walks no feature can predict anything, so runbook 09 left that open. This runbook plants a signal that only memory can find: the log price is a random walk plus a hidden AR(1) mispricing (, half-life about 23 bars), so the expected five-bar return is . A classifier on the hidden (the oracle, not tradable) bounds what any feature set can reach.
The generator’s strength was calibrated on the oracle only, and every setting tried is disclosed in the notebook. No candidate feature set had been run when the hypotheses were written.
Hypotheses (pre-registered)
Section titled “Hypotheses (pre-registered)”“FFD” is ffd_dstar: the FFD log price at (thresh = 1e-3), with chosen on
each training fold by runbook 09’s ADF rule. “Returns” is the last five log returns. “Integer
differences” is the log price differenced over 1, 5, 20 and 60 bars. Each is the input of the same
L2-penalised logistic regression. Out-of-fold probabilities come from purged 5-fold CV with a 1%
embargo. Each test is a paired one-sided -test over 30 paths at the headline strength
.
- H1. FFD’s out-of-fold AUC is higher than returns’.
- H2. FFD’s net Sharpe ratio (5 bps per unit turnover) is higher than returns’.
- H3. FFD’s AUC is higher than the integer differences’.
- H4. On the headline path, FFD’s deflated Sharpe ratio over all 8 registered trials is at least 0.95.
- Controls. At the H1-H3 differences must not be significant (), and on
the
SYN_*sample no trial may reach a DSR of 0.95. - Decision rule. Promote only if H1 and H2 hold and both controls pass.
Method
Section titled “Method”- Labels: one event per bar, if the five-bar forward log return is positive (fixed-horizon labels, AFML §3.2). The label spans .
- Feature sets (8 trials):
returns,int_diff,level(), FFD at fixed , andffd_dstar. Every one uses closes up to only. - Classifier: L2-penalised logistic regression on features standardised with the training fold’s statistics. It is written in numpy because scikit-learn is not a dependency, and it is the same model as runbook 11.
- Bets: held for five bars, overlapping bets averaged. Net returns are , annualised by because the bars are simulated business days.
- Registry: every trial is recorded in an
openquant.evaluation.TrialRegistry. The headline path and theSYN_*control each have their own registry.
Results (SYNTHETIC)
Section titled “Results (SYNTHETIC)”| Test (30 paths) | (signal) | (null) |
|---|---|---|
| H1: AUC, FFD − returns | +0.040, = 7.4 | +0.024, = 4.6 (control fails) |
| H2: net Sharpe, FFD − returns | +0.47, = 6.1 | +0.27, = 3.4 (control fails) |
| H3: AUC, FFD − integer differences | +0.029, = 4.7 | +0.023, = 2.9 (control fails) |
H4: on the headline path FFD’s net Sharpe ratio is 0.60 (PSR 0.951) and its DSR over 8 trials is
0.779, so H4 is rejected. The SYN_* control passes as registered, with a highest DSR of 0.863.
What it means
Section titled “What it means”- Most of the gap is there without a signal. H1-H3 pass at the planted strength, but with no signal FFD is already ahead by most of the same margin. Over the null, the planted signal adds only +0.016 of AUC and +0.20 of Sharpe ratio.
- Purged k-fold flatters memory and handicaps returns (post hoc, walk-forward). On random
walks, k-fold gives the
levelclassifier a net Sharpe ratio of +0.22 andffd_dstar+0.17. The model for a middle fold is fitted on bars after it too, and a feature that carries the price level lets it use where the path went. Purging removes label overlap, not this. Returns sit below chance (AUC 0.478, the base-rate effect of runbook 11). Walk-forward removes the Sharpe bias: FFD minus returns is −0.01 ( = −0.1) at , and +0.35 ( = 3.3) at the planted strength. The AUC gap survives the walk-forward null (+0.017, = 3.4), so pooled out-of-fold AUC is not a clean yardstick for memory features. - FFD is the level in all but name. The ADF rule picks , where the truncated
weights pass 37% of the log price through.
ffd_dstarandlevelscore the same: k-fold AUC 0.530 and 0.529, walk-forward net Sharpe ratio 0.18 and 0.16. - On random walks, k-fold nearly made a false discovery. On the
SYN_*sample,levelhas a net Sharpe ratio of 1.85 andffd_d0.21.44. Only the deflation keeps them below 0.95.
Decision
Section titled “Decision”Do not promote. FFD features with chosen per training fold do not become the default classifier input over returns or over integer differences. The pre-registered null control fails. Later runbooks should:
- evaluate memory-bearing features walk-forward, or with CPCV and a null run through the same splits;
- judge them on net returns against that null, not on pooled AUC;
- compare FFD with the level itself, not only with returns;
- pre-register the walk-forward comparison (FFD vs returns vs level, net Sharpe ratio, with the null) as the next test, on real data.
Checks
Section titled “Checks”The notebook asserts, as executed code:
- Features are causal. No feature, and no FFD series for any , changes when every close after a cut is replaced.
- Purge. Train/test label overlaps are zero in every fold; unpurged k-fold has 5-10.
- The test fold is unseen. Each fold’s chosen and fitted model are bit-identical when the test fold’s labels are replaced with noise.
- Shift test. On a zero-signal path, the same features moved one bar into the future lift the AUC from 0.50 to 0.69 (returns) and from 0.51 to 0.68 (FFD at 0.4).
Trial count. 8 per registry, all pre-registered, all deflated by. The Monte Carlo paths, the oracle and the post hoc walk-forward runs, which were made on the Monte Carlo paths only, are not trials.
Follow-up (#217)
Section titled “Follow-up (#217)”The bias this runbook found is now documented and tested in
cross_validation:
the model for a middle fold learns to fade the level against a training mean that includes the
levels after the fold. cross_validation.walk_forward_splits gives the forward-only splits the
post hoc check used, and cross_validation.null_score_distribution runs a no-signal null through
any splits. The walk-forward AUC gap is a property of pooled AUC, which ranks events at different
times, not a leak. Runbooks 11 to 13 were audited for the same bias; none of their conclusions
changes. The notebook contract
now requires the null control for features with memory.
Run it on your own data
Section titled “Run it on your own data”The control study reads four environment variables; the planted-signal study does not depend on the data source. Write the executed copy and figures outside the repository:
OPENQUANT_RUNBOOK_SOURCE=/path/to/daily_ohlcv.parquet \OPENQUANT_RUNBOOK_SYMBOLS=ES,NQ,CL \OPENQUANT_RUNBOOK_RANGE=2010-01-01:2024-12-31 \OPENQUANT_RUNBOOK_REGISTRY=$HOME/.openquant/trials-fracdiff-classifier.json \OPENQUANT_FIGURE_DIR=/tmp/fracdiff-classifier-figures \ uv run --python .venv/bin/python python notebooks/python/scripts/execute_notebook_cells.py \ notebooks/python/14_fracdiff_features_classifier.ipynb --out /tmp/14_on_my_data.ipynbOPENQUANT_RUNBOOK_REGISTRY keeps the control’s trial registry between sessions, so every feature
set you try on your data counts toward its deflated Sharpe ratio. OPENQUANT_RUNBOOK_REPS sets
the number of Monte Carlo paths per strength (default 30). On your data, read the control table
next to the zero-signal Monte Carlo: purged k-fold favours level-like features even when there is
nothing to find.
Reproducibility
Section titled “Reproducibility”The notebook’s last cell prints the dataset_hash of the sample (sha256:b311c219…), the seed
(209), a digest of the configuration and the trial counts (8 on the planted path, 8 on the
control, 90 Monte Carlo paths). The git commit, the package versions and the hash of the simulated
path go to stderr.