volHarvester — Fixes
Every defect the audit found and how it was repaired — the same measurements as the engineering record, without the code.
4 issues found and repaired — 3 defects, 1 warning.
| # | Issue | Severity | Surface |
|---|---|---|---|
| 1 | The header claimed a variance risk premium it cannot capture, and an estimator it does not implement | DEFECT | documentation |
| 2 | The DCA ladder could never advance past its first rung | DEFECT | behaviour |
| 3 | Any setting typed as 0 was silently ignored — including the harness's own | DEFECT | user inputs |
| 4 | Eight numbers on the side panel had no explanation | WARNING | side panel |
Every fix was verified by re-running the full backtest across all 7 markets and comparing the result record against the pre-fix baseline. Where a fix is stated to change no behaviour, the two records are identical to the byte.
The header claimed a variance risk premium it cannot capture, and an estimator it does not implement
Severity DEFECT · Surface documentation · Sites 4
The header titled this strategy "Spot Variance Risk Premium Capture", described it as "Delta-Neutral", cited Carr & Wu (2009) on the variance risk premium, Bollerslev et al. (2009) on vol-of-vol risk premia and Gatheral (2006) on volatility surface modelling, and listed five ensemble estimators including Yang-Zhang.
Why it matters. Its own successor says so. volRegimeMM — the v2.0 rename of this exact file — states in its header that the earlier name "with citations to Carr-Wu (variance risk premium) and Bollerslev et al. (vol-of-vol risk premia) misrepresented what this strategy actually does", because capturing a variance risk premium requires options or variance swaps that Gunbot cannot access.
The measurement agrees. "Delta-neutral" implies transient, self-cancelling exposure; this strategy is in a long spot position on 99.5% of bars and has no stop-loss sell. Its directional exposure is the whole position, not a residual.
And the estimator list was wrong in both directions: it named Yang-Zhang, which is not implemented — yzVol was declared in the persisted store and never computed or read — while omitting two estimators that are in the ensemble.
Claimed versus implemented
| Estimator | claimed in the header | in the volEstimates array |
|---|---|---|
| Parkinson (high-low) | yes | yes |
| EWMA (λ = 0.97) | yes | yes |
| Return-based rolling std | not listed | yes |
| ATR-normalised | not listed | yes |
| Yang-Zhang | yes | no — declared in the store, never computed |
The header said five; four are averaged, and only two of the four were named. A reader trusting the citation list would believe the σ estimate is better-founded than it is.
The fix. Retitled to "Realized-Vol-Conditional Spot Strategy", removed the three citations crediting work the strategy does not implement, removed the delta-neutral claim, corrected the estimator list to the four actually averaged, deleted the dead yzVol field, and marked the file SUPERSEDED with a pointer to volRegimeMM and the measured finding that the two are identical.
Effect on live behaviour. None. The removed store field was never read; everything else is comments. Verified by re-running the full 3-year, 7-market backtest and diffing the result record — identical to the byte.
Scope beyond this strategy. The estimator overstatement is also in
volRegimeMM, this strategy's successor, and was corrected there in the same pass. The pack contains two further superseded/replacement pairs that have not been checked: almgrenMicroMM → arrivalPriceMM, gatheralNoArbMM → driftCorrectedMM.
The DCA ladder could never advance past its first rung
Severity DEFECT · Surface behaviour · Sites 1
The strategy advertises a 6-level DCA ladder (VH_MAX_DCA, default 6) with decaying rung sizes (VH_DCA_DECAY, 0.8). Neither worked. The block that handles selling ended with a bare return, so the accumulate path below it was unreachable for as long as a position was held — and this strategy holds one on 99.5% of bars.
Why it matters. dcaLevel is only incremented on the accumulate path, and it is reset to 0 by a full exit. Unreachable-while-holding therefore means it could only ever go 0 → 1 before being reset. Two consequences: the ladder never advanced beyond its first rung, and because the rung size is effectiveTL × DCA_DECAY ^ dcaLevel, 0.8 ^ 0 is always 1 — so the decay never applied either. The side panel showed DCA 1/6 permanently, which reads as "one rung used of six available" when in fact six were never available.
Measured directly: over three years the strategy placed 1 buy on BTC, ETH and LINK, and 2 on ADA, DOGE, SOL and XRP — one per position, every time.
The fix measured at FIXED capital (107 quote) so the code change is isolated from re-calibration
| Market | buys | return | max drawdown | peak deployed |
|---|---|---|---|---|
| ADA | 2 → 6 | 5.5% → 9.5% | 9.4% → 5.2% | 94% → 97% |
| BTC | 1 → 3 | 3.6% → 8.5% | 2.3% → 2.3% | 94% → 94% |
| DOGE | 2 → 6 | 6.1% → 13.3% | 4.3% → 2.5% | 94% → 94% |
| ETH | 1 → 6 | 3.1% → 12.7% | 2.6% → 2.1% | 94% → 97% |
| LINK | 1 → 6 | 3.5% → 14.7% | 4.4% → 3.8% | 94% → 94% |
| SOL | 2 → 11 | 10.0% → 23.7% | 10.8% → 10.8% | 94% → 94% |
| XRP | 2 → 6 | 10.8% → 17.1% | 2.5% → 12.3% | 94% → 94% |
| median | 5.5% → 13.3% | 4.3% → 3.8% |
Median return more than doubles and median drawdown falls slightly, with the strategy still profitable on 7 of 7 markets and peak deployment essentially unchanged. XRP is the only market whose drawdown worsens (2.5% → 12.3%). This is unusual for a change that increases how much a strategy trades, and it is the reason the fix was applied at the shipped default of 6 rather than being neutralised to 1.
The fix. Removed the bare return that closed the sell block, so control falls through to the accumulate path when no sell fired. Every individual sell path inside the block still returns on its own, so a cycle that sells does not also buy.
Effect on live behaviour. This one does change behaviour at the shipped defaults, unlike every other fix in this audit — that is the point of it. The strategy now places 3–12 buys per market over the window instead of 1–2, and the DCA ladder and its decay both function. Measured at fixed capital the change improves median return and median drawdown together. It was applied because the measurement supports it, not because the defect was cosmetic.
Scope beyond this strategy. This shape — a
gotBagblock that handles the sell paths and then returns unconditionally, with the accumulate path below it — needs checking on every Quantroduction strategy that advertises a DCA ladder. It is the same class of defect as the Directional pyramid gate: a documented scaling feature that structurally cannot fire.
Any setting typed as 0 was silently ignored — including the harness's own
Severity DEFECT · Surface user inputs · Sites 19
Every numeric setting was read through a fallback idiom meaning use the operator's value, or the baked default. Zero is falsy in JavaScript, so a setting of zero was replaced by the baked default with no warning — the one input an operator could not express was the one that turns a feature off.
Why it matters. This audit found a second victim of the same bug: the backtest harness itself. It passes WARMUP_CYCLES: 0 to skip the strategy's own cycle-counting warmup, because the engine already withholds 2,600 bars of history. The strategy read that through the same fallback idiom and used 10. So the harness had been asking for no warmup for the entire project, and every Quantroduction strategy had been quietly ignoring it.
Proof that the warmup setting was the only behavioural difference — BTCUSDT, 3 years
| Run | buys | sells | return |
|---|---|---|---|
| original file, harness asks warmup=0 (ignored → 10) | 3 | 16 | 0.95% |
| fixed file, harness asks warmup=0 (honoured) | 1 | 5 | 0.39% |
| fixed file, warmup set explicitly to 10 | 3 | 16 | 0.95% |
The fixed file at warmup=10 reproduces the original exactly, which proves the setting was the whole difference and nothing else in the fix changed behaviour. The harness override is kept at 0: the engine already supplies the history, so the strategy's own counter would only delay the start of the scored window.
The fix. Replaced the idiom with a reader that separates "not set" from "set to zero": it returns the default for undefined, null, "" and a non-numeric value — exactly what the old idiom did in those cases — and the operator's number for everything else, including 0. This also closes the NaN hole, where only the short-circuit stopped a non-numeric setting reaching the arithmetic.
Effect on live behaviour. The 19 operator-facing settings behave identically at every value that previously worked. The measured numbers do move, for one fully explained reason: the harness's WARMUP_CYCLES: 0 is now obeyed. The table above isolates that.
Scope beyond this strategy. Pack-wide: 1,743 sites across 70 files. Every Quantroduction strategy reads its settings this way.
Eight numbers on the side panel had no explanation
Severity WARNING · Surface side panel · Sites 8
Side-panel rows including State, Gain, DCA, Round Trips and Max DD carried an empty tooltip or none at all.
Why it matters. The side panel is where an operator reads what a strategy is doing without opening its code. An unexplained number is not information, it is something to guess at — and DCA 1/6 was actively misleading, since six rungs were never reachable.
Side-panel rows on this strategy
| before | after | |
|---|---|---|
| data rows | 20 | 20 |
| with a tooltip | 11 | 19 |
| with no tooltip | 9 | 1 |
The remaining row is Holding, left deliberately unexplained: across the pack that label is used both for a yes/no and for a quantity, so no single wording is true everywhere.
The fix. Filled from the shared side-panel label glossary built during this audit. A label the glossary does not know is reported rather than guessed at.
Effect on live behaviour. None. Tooltips are display text.
Scope beyond this strategy. Pack-wide: 1,433 rows over 392 distinct labels.