volRegimeMM — Fixes
Every defect the audit found and how it was repaired — the same measurements as the engineering record, without the code.
5 issues found and repaired — 4 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 |
| 5 | One exit path put the volatility regime where the realised gain goes, and 26 exits vanished from the record | DEFECT | 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.
One exit path put the volatility regime where the realised gain goes, and 26 exits vanished from the record
Severity DEFECT · Surface side panel · Sites 1
This strategy has three ways of closing a position, and it announces each one on the side panel in a fixed shape — the reason, then the gain it realised, then the cash result. Two of the three follow it. The third, the volatility-harvest exit, put the volatility regime in the slot where the gain belongs.
That slot is the one the audit reads to work out why each exit happened, so every exit taken by that path went unaccounted for — 26 of the 31 unexplained exits across three years and seven markets.
It could not be recovered from the log either, because that path and the take-profit path print exactly the same console line. Nothing anywhere in the strategy's output separated the two, or said what either of them made.
Why it matters. An exit that cannot be named cannot be counted, and the counts are how this strategy is understood. With 35% of exits unattributed, the ladder could not show that eight emergency stops averaging -19.40% were cancelling forty-nine take-profits averaging +2.54% — which is the entire result. An operator watching the panel live had the same gap: they could see a sale had happened and not whether it made money.
Exits the strategy could not account for, 3 years x 7 markets
| Exit path | exits | share of all sells | named on the panel? |
|---|---|---|---|
TP SELL |
49 | 55.7% | yes — +2.54% mean |
STOP LOSS |
8 | 9.1% | yes — -19.40% mean |
VOL SELL |
26 | 29.5% | no — regime printed where the gain goes |
resting fill (Fill confirmed (age)) |
5 | 5.7% | no — separate, already-recorded class |
Before the repair the bottom two rows were one undifferentiated block of 31 unexplained exits (35.2%). Naming the volatility path leaves 5, which is the resting-fill class and not this one.
The fix. Restore the file's own convention — the gain first, then the cash result, then the volatility regime it was already reporting, which is worth keeping and was simply in the wrong place. The exit now reads as a gain of so much, a net of so much, in such-and-such a regime.
The repair matches the shape of the announcement rather than its exact wording, so the same slip written slightly differently elsewhere is caught too, instead of the tool reporting that it found nothing.
Re-measured after the change, 3 years x 7 markets at 15m
| Market | buys | sells | return |
|---|---|---|---|
| ADA | 11 → 11 | 8 → 8 | -12.81% → -12.81% |
| BTC | 11 → 11 | 9 → 9 | -9.15% → -9.15% |
| DOGE | 12 → 12 | 16 → 16 | 3.54% → 3.54% |
| ETH | 11 → 11 | 10 → 10 | -10.52% → -10.52% |
| LINK | 11 → 11 | 11 → 11 | -16.82% → -16.82% |
| SOL | 17 → 17 | 22 → 22 | -7.94% → -7.94% |
| XRP | 11 → 11 | 12 → 12 | -22.59% → -22.59% |
Effect on live behaviour. None. No price, size, threshold or gate is touched — only the text of a notification. The re-measurement after this fix must return byte-identical trades and returns on all seven markets, and is reported here as the proof that it did.
Scope beyond this strategy. Found on volHarvester and volRegimeMM, which are the same code, which are the same code. The shape it breaks — a side-panel exit notification written as
REASON: gain% | net— is the convention the whole Quantroduction lineage follows and the one the audit tooling reads to build the exit ladder, so the same slip elsewhere would have the same effect.