volRegimeMM — 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 DCA ladder could never advance past its first rung | DEFECT | behaviour |
| 2 | The header claimed five volatility estimators; four are implemented | DEFECT | documentation |
| 3 | Any setting typed as 0 was silently ignored — including the harness's own | DEFECT | user inputs |
| 4 | Nine 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 DCA ladder could never advance past its first rung
Severity DEFECT · Surface behaviour · Sites 1
The strategy advertises a 6-level DCA ladder (VR_MAX_DCA) with decaying rung sizes (VR_DCA_DECAY). Neither worked. The block handling sells ended with a bare return, so the accumulate path below was unreachable while a position was held — and this strategy holds one on essentially every bar.
Why it matters. dcaLevel is only incremented on the accumulate path and is reset to 0 by a full exit, so unreachable-while-holding means it could only ever go 0 → 1. Two consequences: the ladder never advanced, and since a rung is sized effectiveTL × DCA_DECAY ^ dcaLevel, DCA_DECAY ^ 0 is always 1 — the decay never applied either. The side panel read DCA 1/6, which looks like one rung used of six available when six were never reachable.
The fix at FIXED capital (107 quote), isolating the code change from re-calibration
| Market | buys | return | max drawdown |
|---|---|---|---|
| ADA | 2 → 6 | 5.5% → 9.5% | 9.4% → 5.2% |
| BTC | 1 → 3 | 3.6% → 8.5% | 2.3% → 2.3% |
| DOGE | 2 → 6 | 6.1% → 13.3% | 4.3% → 2.5% |
| ETH | 1 → 6 | 3.1% → 12.7% | 2.6% → 2.1% |
| LINK | 1 → 6 | 3.5% → 14.7% | 4.4% → 3.8% |
| SOL | 2 → 11 | 10.0% → 23.7% | 10.8% → 10.8% |
| XRP | 2 → 6 | 10.8% → 17.1% | 2.5% → 12.3% |
| median | 5.5% → 13.3% | 4.3% → 3.8% |
Median return more than doubles while median drawdown falls slightly, and the strategy stays profitable on 7 of 7. XRP is the only market whose drawdown worsens. That is why the fix was applied at the shipped default of 6 rather than neutralised to 1.
The fix. Removed the bare return closing the sell block so control falls through to the accumulate path when no sell fired. Each individual sell path still returns on its own, so a cycle that sells does not also buy.
Effect on live behaviour. This fix does change behaviour at the shipped defaults — that is its purpose. The strategy now places 3–12 buys per market over the window instead of 1–2, and the ladder and its decay both function. It was applied because the measurement supports it on both return and drawdown.
Scope beyond this strategy. 15 strategies in the pack carry this exact shape — a
gotBagblock that handles the sell paths and then returns unconditionally, with adcaLevel < MAX_DCAaccumulate path stranded below it: atrChannelMM, bbSqueezeMM, dipHunter, emaBounceMM, ichimokuCloudMM, keltnerChannelMM, meanRevOscillator, regimeArbitrageur, rsiMeanRevMM, stochMeanRevMM, superTrendMM, volHarvester, volRegimeMM, vwapMM, williamsPctRMM. Two are fixed so far.
The header claimed five volatility estimators; four are implemented
Severity DEFECT · Surface documentation · Sites 3
The header listed five estimators in the ensemble and stated "All 5 estimators are faithfully implemented from their respective papers". It also carried a citation block crediting Garman & Klass (1980) and Yang & Zhang (2000) as "one of the 5 in the ensemble" each.
Why it matters. The ensemble is a plain equal-weighted mean of whatever is in the volEstimates array, and that array holds four terms: Parkinson, return-based standard deviation, an ATR-normalised estimate, and an EWMA. Yang-Zhang is not implemented — yzVol was declared in the persisted store and never computed or read anywhere in the file. Garman-Klass is not implemented either — it appears nowhere in the code except its own citation.
This matters more here than it would elsewhere. The entire stated reason for the v2.0 rename of this strategy was that its predecessor's documentation "misrepresented what this strategy actually does". Correcting one overstatement while introducing another defeats the purpose of the rename, and a reader who trusts the citation list would believe the σ estimate is better-founded than it is.
Claimed versus implemented
| Estimator | claimed in the header | in the volEstimates array |
|---|---|---|
| Parkinson (high-low) | yes | yes |
| Return-based rolling std | yes | yes |
| ATR-normalised | yes | yes |
| EWMA (λ = 0.97) | yes | yes |
| Yang-Zhang | yes | no — yzVol declared, never computed |
| Garman-Klass | cited as one of the five | no — absent from the file entirely |
The fix. Corrected the header to state the four estimators actually averaged, removed the two citations that credited unimplemented work, and deleted the dead yzVol store field. The correction is recorded in the header rather than applied silently, and adding a genuine Garman-Klass or Yang-Zhang term is noted as a future change that would alter behaviour and so needs measuring.
Effect on live behaviour. None. The removed field was never read and the rest 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 same overstatement is in
volHarvester, this strategy's superseded predecessor, and was corrected there in the same pass.
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. The same bug was also discarding the backtest harness's own WARMUP_CYCLES: 0, which it passes because the engine already withholds a 2,600-bar warmup. The strategy read it through the same fallback idiom and used 10. So the harness had been asking for no warmup for the whole project and every Quantroduction strategy quietly ignored it.
Why the measured numbers moved — BTCUSDT, 3 years, warmup isolated
| Run | buys | sells | return |
|---|---|---|---|
| original, harness asks warmup=0 (ignored → 10) | 3 | 16 | 0.95% |
| fixed, harness asks warmup=0 (honoured) | 1 | 5 | 0.39% |
| fixed, 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 this fix touched behaviour. The override stays at 0: the engine 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": the default for undefined, null, "" and a non-numeric value — exactly what the old idiom gave 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 move for one fully isolated reason, shown above.
Scope beyond this strategy. Pack-wide: 1,743 sites across 70 files.
Nine 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 the code. An unexplained number is something to guess at — and DCA 1/6 was actively misleading, since six rungs were unreachable.
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. 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.