Home Verified Methods Reports Papers Methodology About
HomeVerified methods › Degenerate inputs: what the implementations return when the answer is obvious

Degenerate inputs: what the implementations return when the answer is obvious

Short answer

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.

InputExpectedBVCTick rule
Monotone up (every trade higher than the last)VPIN ≈ 10.99990.9999
Monotone downVPIN ≈ 10.99990.9999
Flat (price never moves)VPIN 0: no information0.00001.0000 → 0.0000 after fix
Alternating ±1 ticknear 0: signs cancel, bucket-to-bucket change tiny0.00000.0001
Random walk, no driftnoise floor (0.45; √(2/π)/√20 = 0.18)0.44850.2055
One trade larger than five bucketssplit across buckets, volume conserved5 complete buckets, VPIN 1.0 (price rose across all of it)
Fewer buckets than the windowno value, not a partial-window numberNaN over 5 buckets
Total volume below one bucketrefuseValueError: 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.

InputExpectedObserved
Single assetrefuseValueError: need at least 2 assets
Two identical assets0.5 / 0.50.500 / 0.500
One zero-variance assetrefuse, naming the columnscipy: “condensed distance matrix must contain only finite values” → ValueError: zero-variance column(s) ['Z'] after fix
NaN in returnsrefuseValueError: returns contain NaN
Perfectly anti-correlated pair plus twosums to 1, no shortsA 0.412, B 0.428, C 0.024, D 0.136
Rank-one panel (four assets = β × one factor)weights without inversion0.542 / 0.241 / 0.132 / 0.084
Same panel, minimum variance via pseudo-inverseone of infinitely many solutions0.143 / 0.214 / 0.286 / 0.357
All returns shifted by +1identical 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, γ₁, γ₂)ExpectedObserved
SR equals the benchmark (1.0, 1.0, 24, 0, 3)exactly 0.50.5000
n = 2, the smallest admissible (1.5, 0, 2, 0, 3)z = 1.5/1.4577 = 1.029, PSR 0.8480.8483
n = 1 (1.5, 0, 1, 0, 3)√0 = 0, z = 0: 0.5 regardless of SR0.5000; must be refused upstream (the calculator does)
Non-positive variance term (1.5, 0, 24, 3, 3)no answerNaN
Kurtosis below 1 (1.5, 0, 24, 0, 0.5), impossible for any distributioncomputes; input invalid1.0000
n = 1,000,000 with SR 0.05saturates1.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

Reproduce

cd quantmedia-research/degenerate-inputs
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.