rollsig · working notes · part II

The Benchmark

Part II of the working notes — v0.2, the Optiver realized-volatility study. Part I (the algebra, and building v0.1) is in docs/notes.html. Last updated 28 July 2026.
Chapter 6

Picking the Fight

6.1 Why realized volatility is the sharpest possible test

Most "do these features help?" questions are vague, because the feature family and the target have no particular relationship and you are really just asking whether a large pile of numbers happens to correlate with something. Realized volatility is different, and it is the reason this benchmark was chosen as the headline study rather than a daily-bar one.

§1.5 established that the lead-lag transform makes quadratic variation visible at level 2. Stated precisely: for the lead-lag embedding of a scalar path, the antisymmetric part of the second level — the Lévy area, doubled — equals the path's discrete quadratic variation exactly.

S(1,2)S(2,1)=i(Δxi)2

Not approximately. On a 200-point random walk, computed through rollsig's numpy backend:

QV               184.8037480539841
S12 - S21        184.8037480539842
ratio            1.0000000000000007
verified — ratio 1 to 15 significant figures

This makes the benchmark's question unusually clean. Realized volatility is not something bolted onto signature features from outside; it is a coordinate of the feature vector. The classical estimator is contained in the family by construction. So the question is not the mushy "are signatures useful for volatility" but the sharp one:

The actual question: level 2 already contains the classical estimator. Does anything above level 2 — the finer description of how the price got where it got — forecast the next ten minutes better than the classical estimator alone? That has a yes or a no, and either is publishable.

The Optiver Realized Volatility Prediction dataset supplies the rest of what a real test needs: genuine order-book paths for 112 stocks, roughly 3,830 ten-minute segments each, a fixed public metric, and a body of published top solutions to reproduce as baselines rather than invent.

6.2 Two backends, two bases

The first thing this benchmark did was make the v0.1 backend choice untenable. Feature building needs one log-signature per segment per window: 20 stocks × ~3,830 segments × 3 windows is roughly 230,000 calls, before the depth study multiplies it again. Timed on a 600-point lead-lag, time-augmented path (1,199 points, dimension 4):

depthcoordsRoughPyiisignatureratio
2108.46 ms0.02 ms×423
33012.19 ms0.11 ms×111
49033.71 ms0.53 ms×64

At RoughPy's depth-3 rate the feature build alone is 47 minutes single-threaded; at iisignature's it is 25 seconds. That is the difference between a study you can iterate on and one you run overnight and stop questioning.

So iisignature became a second log_signature backend. It did not go in quietly, because the two backends do not return the same numbers — and this is the interesting part.

not a bug — a convention difference worth pinning down

Both libraries return coordinates of the same Lie element, but in different bases of the free Lie algebra. On a random 9-point path in dimension 3 at depth 3, the first six coordinates — levels 1 and 2 — agree to machine precision. Level 3 does not:

iisignature  ... -3.27487 -1.32135 -2.54993 -0.65885  1.14639 -0.39506
roughpy      ...  3.27487  1.22858  1.14639  1.32135  0.65885  0.39506

The same magnitudes appear in both, permuted and sign-flipped: different Lyndon/Hall bracket orderings and different sign conventions for the nested brackets. Neither is wrong. Both are legitimate coordinate systems on the same 14-dimensional space.

Silently accepting that would be a real hazard — a model fitted on one backend's columns and scored on the other's would produce plausible-looking garbage. Two things were done instead.

First, a basis-independent oracle. _log_signature_expanded maps either backend's output back into canonical tensor-word coordinates — via iisignature's expanded method for one, ctx.lie_to_tensor for the other — where the redundant-but-canonical representation makes them directly comparable. There, both must equal _log_signature_full_numpy, which computes log(S) as a power series and involves no basis at all. Both do, to 1e-8. That is what establishes that each backend really is returning a log-signature and not something merely log-signature-shaped.

Second, a test that asserts the disagreement itself: level 1 and 2 must match, level 3 must not. If a future release of either library quietly changed convention, that test fails loudly rather than the benchmark drifting.

tests/test_backend.py — both backends expand to the numpy oracle; divergence pinned from level 3

RoughPy stays the default for log_signature, matching signature; the benchmark asks for iisignature explicitly. The column names come from whichever backend produced them, so get_feature_names_out can never mislabel.

6.3 An irregular stream, and why forward-filling is free

The raw book data is an event stream: a row appears only when the book changed, so seconds_in_bucket is sparse and irregular. Everything downstream wants a regular one-second grid, which means forward-filling — and forward-filling data before feeding it to a model is normally the sort of thing that deserves suspicion.

Here it is provably free, and the reason is §5.1. The signature is invariant under reparametrization: subdividing a segment, or repeating a point, does not change it. Forward-filling inserts no new geometry — it repeats the standing price until the next update, tracing the identical trajectory. The price path the signature sees is literally unchanged.

What does change is the time channel added by time_augment, and it changes for the better. On the raw event stream, a uniformly spaced time channel measures event count — it would say two quiet minutes and two frantic ones took the same amount of time. On the filled grid it measures real elapsed seconds. So the resampling is not a compromise made for convenience; it is what makes the time augmentation mean what it is supposed to mean.

Worth noticing: this is the first place in the project where a theorem from Part I did practical work in a design decision rather than in a test. Reparametrization invariance is why a preprocessing step that would otherwise need defending needs none.
Chapter 7

Designing a Comparison That Can Fail

7.1 The arms, and the one thing allowed to vary

A feature-comparison benchmark is worth exactly as much as its control. If the signature arm gets a different learner, different hyperparameters, different folds or even a different row ordering than the baseline, then a favourable number says nothing about signatures.

So: one learner (HistGradientBoostingRegressor, fixed hyperparameters, no tuning), one set of folds, one feature table built in a single pass so every arm sees byte-identical rows in identical order. The only thing that varies between arms is which columns are selected.

armwhat it is
naivepredict the observed window's realized volatility. No model at all. Volatility is strongly persistent, so this is a genuinely hard floor.
harHAR-RV-style: realized volatility over nested suffix windows (600/300/150/60/30 s), plus activity and quarticity terms.
bookreproduced top-solution-style aggregates: WAP realized volatility at both book levels, relative spread, depth imbalance, trade intensity, over nested windows.
sigrollsig log-signatures of the lead-lag, time-augmented log-WAP path over the same nested windows.
sig+har, sig+book, sig+book+harthe marginal-value arms — what signatures add on top of a baseline.

Those last three matter more than the headline. A general feature family beating a specialised baseline outright is rare and would be a surprising claim; adding information at the margin is the realistic and still-useful outcome, and it is the one the combined arms measure. Note also that sig is given the same multi-horizon window structure as har, so the comparison tests the feature family rather than the horizon choice.

stock_id goes to every arm equally — each stock has its own baseline volatility level, and withholding that would handicap all arms identically but pointlessly.

7.2 RMSPE, and training on the metric rather than near it

The competition scores root mean squared percentage error. Dividing by the truth means a miss on a quiet segment counts as heavily as the same relative miss on a violent one — which is the right call for volatility, where the quantity ranges over orders of magnitude and a proportional error is what a desk actually cares about.

Training on raw squared error would therefore optimise something the model is not scored on: it would lavish attention on the high-volatility segments where absolute errors are large and neglect the quiet ones entirely. The fix is a one-liner and worth spelling out because it is exact rather than a heuristic:

iwi(yiy^i)2withwi=1/yi2=n·RMSPE2

Weighting each sample by the reciprocal of its squared target turns ordinary squared error into precisely the competition metric. The learner optimises what it is judged on, in every arm equally.

Early stopping is switched off deliberately. scikit-learn's internal validation split is random, and a random split would cut straight across the time_id grouping that the next section exists to protect — a leak smuggled in through a convenience flag.

7.3 The leak that matters — and the split that isn't possible

The plan for this benchmark asked for strict walk-forward cross-validation. On this dataset that is not achievable, and saying so is more useful than pretending otherwise.

The organisers deliberately shuffled and anonymised time_id, so its numeric order is not chronological. There is no timestamp anywhere in the shipped data. A genuine walk-forward split — train on the past, test on the future — cannot be reconstructed from what the competition provides. (Some competitors did reconstruct an ordering from cross-stock volatility correlations; that is a competition-specific exploit, not a foundation for an honest benchmark, and it is out of scope here.)

What is both reconstructable and essential is grouping. A time_id is one instant of market time observed simultaneously across all 112 stocks. Realized volatility is strongly correlated across stocks at the same instant — when the market is turbulent, it is turbulent for everyone. So if one stock's row from a given time_id trains a model that is then scored on a different stock's row from the same time_id, the model has effectively been shown the answer.

GroupKFold on time_id closes that, and a test asserts it directly rather than trusting the class name:

for train_idx, test_idx in evaluate.folds(groups, n_splits=5).split(X, y, groups):
    assert set(groups[train_idx]).isdisjoint(groups[test_idx])
tests/test_orvp.py — no time_id straddles a fold boundary
Stated as a limitation, not solved: this is grouped cross-validation, not walk-forward. It removes the cross-sectional leak, which is the one that would dominate here. It does not simulate deployment across time, and no number produced by it should be read as if it did.

7.4 Causality as a test, not a claim

"No lookahead" is the easiest claim in quantitative research to make and the easiest to be quietly wrong about. Part I tested it for the rolling transformer; the benchmark needs its own version, because the failure mode here is different — an off-by-one in a window slice, or a groupby that silently spans segments.

The test is adversarial rather than assertive. Build one stock's features. Then rebuild them from a dataset in which every later segment's book prices have been overwritten with garbage, and demand that the earlier segments' features are unchanged:

book.loc[book["time_id"] >= N // 2, price_columns] = 999.0
...
pd.testing.assert_frame_equal(original.features.iloc[early], corrupted.features.iloc[early])

If any feature reaches across segment boundaries, corrupting the future moves the past and the test fails. On real data that reach would be a reach into the future.

tests/test_orvp.py — features depend only on the segment they describe

Two smaller traps get their own tests for the same reason. A suffix-window test confirms the 300-second window genuinely ignores everything in the first 300 seconds — the kind of thing an off-by-one turns into a silent full-window duplicate. And a test asserts the 600-second and 150-second signature blocks actually differ, which would catch a slicing bug that turned the multi-horizon arm into three copies of the same numbers wearing different column names.

Chapter 8

Results

8.1 The table

20 stocks, 76,599 ten-minute segments, depth-3 log-signatures over 600/300/150-second causal suffix windows, five grouped folds, one shared learner. The whole feature table — every arm's columns — builds in 21 seconds.

armfeaturesRMSPEfold std
naive (observed window's RV)10.33485
har270.240750.00222
book370.231770.00207
book+har630.230930.00238
sig910.243620.00212
sig+har1170.239800.00212
sig+book1270.231990.00212
sig+book+har1530.231330.00218

First, the sanity check: the reproduced baselines work. book+har beats the naive persistence predictor by 31.0% (bootstrap CI on the absolute delta [+0.0978, +0.1098], p = 0.000). Whatever the signature arm is being compared against, it is not a strawman.

Now the actual answer, in three parts.

Signatures alone nearly match hand-designed volatility features. 91 log-signature coordinates reach 0.24362 against the 27-feature HAR-RV set's 0.24075 — 1.19% worse. What makes that interesting is not the sign but the size: the signature arm was never told what realized volatility is. It received the geometry of a price path and a truncation depth. A generic construction landing within about one percent of features designed specifically for this target is the genuinely notable number in the table.

They add a little to a weak baseline. sig+har improves on har by 0.40%. So the signature coordinates are not pure redundancy with respect to multi-horizon RV; there is something in them that HAR-RV does not have.

They add nothing to a strong one. sig+book+har scores 0.23133 against book+har's 0.23093 — 0.17% worse, with the group bootstrap putting the whole 95% interval on the wrong side of zero (p(no improvement) = 0.992). Against the order-book baseline, 91 extra columns are cost without benefit.

The headline, stated the way §8.3 requires: on the competition training data, with the competition metric, under grouped cross-validation with a fixed shared learner, adding depth-3 log-signatures to the best reproduced baseline made RMSPE 0.17% worse. Signatures did not earn their place on this task.
v0.2.1 addendum — these numbers are one stock subset

Everything above was computed on a single 20-stock subset (seed 0). v0.2.1 reran the whole benchmark on three pre-registered subsets, and seed 0 turned out to be the most favourable of the three. sig+book+har vs book+har is −0.17% on seed 0 but −1.78% and −1.02% on seeds 1 and 2 — mean −0.99%. The sign of the conclusion is unchanged and the magnitude is understated here; the three-subset table in benchmarks/orvp/results/multiseed_table.md supersedes this one.

The 0.40% sig+har gain in the paragraph above does not survive replication at all: it is −2.86% and −1.96% on the other two subsets. Read "there is something in them that HAR-RV does not have" as a statement about seed 0, not about the task.

8.2 Depth and window length: where it turns over

Signature feature count grows fast in the truncation depth — the free Lie algebra on d generators has roughly dn/n independent brackets at level n. The question is where the extra coordinates stop describing anything a ten-minute volatility forecast can use. Same folds, same learner, signature columns only.

depthwindows (s)featuresRMSPEfold std
2600110.251340.00294
2600-300-150310.245100.00234
2600-300-150-60410.244910.00237
3600310.247000.00263
3600-300-150910.243620.00238
3600-300-150-601210.243480.00242
4600910.246270.00244
4600-300-1502710.243540.00250
4600-300-150-603610.243520.00264

Depth turns over at 3. Going from depth 2 to depth 3 buys a real improvement (0.24510 → 0.24362 on the three-window set). Going from 3 to 4 triples the feature count — 91 to 271 — and moves RMSPE by 0.00008, which is a thirtieth of the fold standard deviation. Level-4 brackets are describing path structure that this target cannot use.

The most useful line in the table is a coincidence of feature counts. Depth 4 over a single 600-second window and depth 3 over three nested windows both produce exactly 91 features. They do not perform alike:

91 features, spent on…RMSPE
depth — level 4 of one window0.24627
horizons — level 3 of three windows0.24362
At a fixed feature budget, buy horizons rather than depth. The same 91 columns are 1.1% better spent on multiple time scales than on deeper iterated integrals of one. Volatility forecasting wants to know how the recent past differs from the less-recent past — which is a statement about windows — more than it wants a finer description of any single window's geometry.

Windows show diminishing returns of their own. Adding the 300- and 150-second suffixes to the 600-second window is worth 1.4% at depth 3; adding a 60-second window on top of those is worth a further 0.06%, comfortably inside fold noise. Three horizons is where this task saturates.

8.3 What this licenses claiming, and what it doesn't

The rule was fixed before any number existed, which is the only time such a rule is worth anything. The private leaderboard was rescored on market data from after the competition closed, so no result obtainable from the training data can be turned into a leaderboard position, and none is. Cross-validation here is grouped, not walk-forward (§7.3), so nothing here simulates deployment across time.

There is a second limit, and it is the more important one because it cuts against the conclusion rather than for it.

This bounds price-path signatures, not signatures. The sig arm received exactly one input: the WAP path. The book arm received order-book state — relative spread, depth imbalance, trade intensity. That information is not in the price path at any truncation depth, so no amount of algebra could have recovered it. The comparison as run is partly a feature-family comparison and partly an input-set comparison, and honesty requires saying which.

So the defensible claim is narrower than "signatures don't help here": signatures of the WAP path alone do not beat order-book features. The experiment that would settle the wider question is a multi-channel signature arm carrying spread and imbalance as additional path channels — at which point both arms see the same information and the comparison is clean. That is the obvious next thing to run, and it is deliberately not being run retroactively to rescue the number.

v0.2.1: that experiment was run, and the caveat did not survive it.

The multisig arm carries log-WAP, relative spread and depth imbalance as three channels of one path (depth 2 — three channels plus time, doubled by lead-lag, is an 8-dimensional path, so the free Lie algebra has 36 coordinates per window rather than 336 at depth 3). Spread and imbalance are computed by the same functions the book arm uses, so the two arms demonstrably read the same quantities.

The confound was real: multisig beats the price-only sig arm on all three subsets (+0.70%, +0.08%, +1.78%), with fewer levels and only 109 features. Giving signatures the order-book state does help them, exactly as this section predicted.

It was not the explanation. multisig+book+har is still worse than book+har on all three subsets (−0.08%, −1.32%, −0.15%; mean −0.52%), and the grouped bootstrap clears p < 0.05 on none of them. The wider claim is therefore now supported rather than merely suspected: on this task, signatures of order-book data do not beat aggregates of the same data, whether or not the signature arm is given the book state. The ORVP feature search is closed on that basis.

Worth noting what §6.1 bought by framing the question sharply in advance. Because the lead-lag identity puts quadratic variation inside the feature family at level 2, a null result is not an absence of evidence. It says something specific: on ten-minute volatility forecasting, the path detail living above level 2 is either not predictive or is already captured by cheaper order-book statistics. That is a finding about how much path geometry this task can use, and it would have been the write-up either way.

Chapter 9

What v0.2 Hands to v0.3

9.1 The redundancy this benchmark pays for three times over

Building the signature arm made the case for a streaming engine concrete in a way the abstract argument for one did not.

Each segment's features come from three nested suffix windows: the last 600 seconds, the last 300, the last 150. The 150-second path is a suffix of the 300-second path, which is a suffix of the 600-second path. Every one is currently computed from scratch, so the final 150 seconds of price data get traversed three separate times, and the total work is 1050 points of path per segment where 600 would do.

Chen's identity (§1.6) says that is unnecessary. The signature of a concatenation is the tensor product of the signatures, so the signature over a suffix can be obtained from the full window's signature by left-multiplying by the inverse of the departing prefix's signature — the group structure of the tensor algebra, doing exactly the work it exists to do. That is the v0.3 streaming engine, and this benchmark is what quantifies its value: not a speedup measured on synthetic paths, but a measured fraction of a real feature pipeline.

Retracted in v0.3 — see docs/notes-streaming.html §11.4. The identity above is correct and the algorithm it suggests saves nothing: recovering the suffixes by inverting departing prefixes needs the full window's signature and every prefix's, which is the same 1,050 points. The saving comes from cutting the longest window into disjoint chunks and combining them forwards, with no inverse involved — group inverses are what a sliding window needs, where the shared part cannot be re-decomposed. Measured, even the chunked route loses by 80× to calling iisignature three times, so this is a redundancy the ORVP pipeline should go on paying.

The other thing v0.2 hands forward is a caution. The nested-window redundancy is arithmetically removable; whether it is removable at acceptable numerical cost is a separate question. Repeated multiplication by group inverses at depth 4 compounds floating-point error in a way that recomputation does not, and the higher levels are precisely where the coefficients are smallest. That was flagged from the start as something to investigate and document rather than assume away. The oracles from Part I are what will settle it: whatever the streaming engine produces has to match a from-scratch recomputation to a stated tolerance, or the speedup is not real.