Degenerate inputs: what the implementations return when the answer is obvious
Twenty-six inputs chosen because their right answer is obvious: monotone, flat and alternating tapes; single, identical, constant and rank-one asset panels; Sharpe ratios at the benchmark, at n = 2, with impossible moments. Running the site's three implementations on them found two more defects the verification reports had missed. A tape whose price never moves read VPIN = 1 under the tick rule, the maximally toxic reading for an input with no information, because the first trade was guessed “buy” and the guess inherited its way through the whole tape. And a panel with one constant asset made HRP fail inside scipy with a message about “finite values” that did not name the cause. Both are fixed and tested; the published report numbers did not move.
Why degenerate inputs
The method note lists degenerate cases as step two. This note is that step carried out systematically. The point of a degenerate input is that the correct output is known before the run, so an implementation cannot pass by accident, and that it exercises the branches ordinary data never reaches: divisions by zero, empty windows, inputs a library will refuse. Every case below was written with its expected behaviour first and the observed output recorded next to it. Where they disagreed, that was the finding.
VPIN
2,000 trades of 100 shares, 20 trades per bucket, a 10-bucket window, both classifiers.
| Input | Expected | BVC | Tick rule |
|---|---|---|---|
| Monotone up (every trade higher than the last) | VPIN ≈ 1 | 0.9999 | 0.9999 |
| Monotone down | VPIN ≈ 1 | 0.9999 | 0.9999 |
| Flat (price never moves) | VPIN 0: no information | 0.0000 | 1.0000 → 0.0000 after fix |
| Alternating ±1 tick | near 0: signs cancel, bucket-to-bucket change tiny | 0.0000 | 0.0001 |
| Random walk, no drift | noise floor (0.45; √(2/π)/√20 = 0.18) | 0.4485 | 0.2055 |
| One trade larger than five buckets | split across buckets, volume conserved | 5 complete buckets, VPIN 1.0 (price rose across all of it) | |
| Fewer buckets than the window | no value, not a partial-window number | NaN over 5 buckets | |
| Total volume below one bucket | refuse | ValueError: no complete buckets | |
The flat tape is the finding. The tick rule signs each trade by comparing its price with the previous one and lets an unchanged price inherit the previous sign; that convention is standard and harmless when prices occasionally repeat. The implementation also gave the first trade, which has no previous price, a sign of +1. On a flat tape that single guess is the only sign there is, every trade inherits it, every bucket is 100% buy, and VPIN reads 1. The fix leaves trades before the first price change unsigned, so they split evenly; a flat tape now reads 0 under both classifiers. The verification report's numbers are unaffected to four decimals because only the synthetic tape's first trade changes sign, and the committed CSV still matches to nine. The random-walk tick reading of 0.2055 against a predicted 0.18 is the autocorrelation of tick-rule signs that the noise-floor note discusses; with 20 trades per bucket it is larger than with 80.
Hierarchical Risk Parity
250 periods; base panel of four assets where A and B load on one factor with opposite signs.
| Input | Expected | Observed |
|---|---|---|
| Single asset | refuse | ValueError: need at least 2 assets |
| Two identical assets | 0.5 / 0.5 | 0.500 / 0.500 |
| One zero-variance asset | refuse, naming the column | scipy: “condensed distance matrix must contain only finite values” → ValueError: zero-variance column(s) ['Z'] after fix |
| NaN in returns | refuse | ValueError: returns contain NaN |
| Perfectly anti-correlated pair plus two | sums to 1, no shorts | A 0.412, B 0.428, C 0.024, D 0.136 |
| Rank-one panel (four assets = β × one factor) | weights without inversion | 0.542 / 0.241 / 0.132 / 0.084 |
| Same panel, minimum variance via pseudo-inverse | one of infinitely many solutions | 0.143 / 0.214 / 0.286 / 0.357 |
| All returns shifted by +1 | identical weights (covariance is shift-invariant) | identical |
Two of these deserve a sentence. The zero-variance case did not silently look sane, which is the property that matters, but the error came from three layers down and blamed the distance matrix; the implementation now checks for constant columns first and names them. The anti-correlated pair is not a defect but a property of the method worth knowing: HRP's distance is √((1 − ρ)/2), so two assets with ρ = −1 sit at the maximum distance and are never clustered together. HRP as specified does not recognise a hedge; it allocates A and B 41% and 43% because each looks like a low-variance asset on its own, and the pair's combined variance never enters the tree.
Probabilistic Sharpe Ratio (closed form)
| Input (SR, SR*, n, γ₁, γ₂) | Expected | Observed |
|---|---|---|
| SR equals the benchmark (1.0, 1.0, 24, 0, 3) | exactly 0.5 | 0.5000 |
| n = 2, the smallest admissible (1.5, 0, 2, 0, 3) | z = 1.5/1.4577 = 1.029, PSR 0.848 | 0.8483 |
| n = 1 (1.5, 0, 1, 0, 3) | √0 = 0, z = 0: 0.5 regardless of SR | 0.5000; must be refused upstream (the calculator does) |
| Non-positive variance term (1.5, 0, 24, 3, 3) | no answer | NaN |
| Kurtosis below 1 (1.5, 0, 24, 0, 0.5), impossible for any distribution | computes; input invalid | 1.0000 |
| n = 1,000,000 with SR 0.05 | saturates | 1.0000 |
| Negative SR (−0.5, 0, 24, 0, 3) | 1 − PSR(+0.5) | 0.0119 |
The closed form has no opinion about whether its inputs are possible: a kurtosis of 0.5 cannot occur (Pearson kurtosis is at least 1) and the formula returns a confident 1.0000 anyway. The calculator on this site refuses n < 2, non-numeric input and a non-positive variance term; it does not yet refuse a kurtosis below 1, which is now on its list. n = 1 returning 0.5 for every SR is mathematically correct and practically a trap.
What this does not establish
- A degenerate suite catches sign, convention and division errors. It does not catch an implementation that is subtly wrong on ordinary inputs and right on the edges; the synthetic-ground-truth tests in the reports are for that.
- “Expected” is a judgement. The flat-tape expectation (VPIN 0) follows from “no information”; someone who defines an unchanged price as a continuation of the last trade's side would defend the old behaviour on a tape that had a first move. The note states its choice.
- Twenty-six cases across three methods is a start, not coverage.
Reproduce
python experiment.py
python ../tests/test_degenerate_inputs.py # expected: 5 passed
python ../tests/test_vpin.py # 15, including the flat tape
Runtime under a second. The two fixes are in vpin.py
(sign_trades_tick_rule) and hrp.py
(hrp_weights), each with the date and reason in its docstring.
Code and output on GitHub.