Docs remediation: the NumPy return type, and snippets that raise when run - #101
Merged
Conversation
Three groups of statements, all made wrong by changes on the current release branch rather than by drift. The O(1) claim. Seventeen of the nineteen occurrences said an update is O(1) per call, which reads as constant work regardless of the configured period and is false for any indicator that needs a full-window pass. `Quickstart-Rust.md` and the numbered contract in `Streaming-vs-Batch.md` already said the true thing -- "O(1) in the input length" -- and are unchanged. The rest now say what actually holds: a tick never revisits the history behind it. The directional warmup. `PlusDm`, `MinusDm`, `PlusDi`, `MinusDi` and `Dx` report `period + 1`, not `period`, because the first candle only seeds the previous close. `Warmup-Periods.md` was self-contradictory about this -- its formula column said `period` while the neighbouring column already said the first value lands on the 15th candle -- and each of the five deep-dives repeated it three times over, including a unit-test value the test no longer produces. The printed outputs. Every block was produced by re-running the page's own Rust example against the built library rather than by editing digits: Bollinger Bands (thirteen numbers), StdDev and VolatilityCone. `M2Measure` was on the list and turned out not to need it -- its value is unchanged, which running it is the only way to establish. Two formula notes went with them. `Indicator-StdDev.md` still described the variance as `Σ price² − mean²`, and `Indicator-FundingRateZScore.md` still warned that this form costs precision and advised comparing at `1e-9`. Both indicators accumulate around a shifted origin now, so the note described a hazard that no longer exists.
Dropping the NumPy dependency changed what `batch` hands back — an
`array.array('d')` for a scalar indicator, a `Matrix` for every shape with more
than one value per bar — but 299 of the 514 deep dives still promised a
`numpy.ndarray`. Anyone following the page reached for `.shape` or a boolean
mask on a type that has neither.
The right replacement depends on the indicator, so it came from the manifest
rather than from the prose: 253 scalar pages, 39 multi-output, 6 profile and 1
bar builder. The qualifiers that stopped being true went with the noun they
qualified, so "a 1-D `float64` `np.ndarray`" reads as "an `array.array('d')`"
rather than leaving `float64` stranded on a type that has no dtype.
Four tails needed more than the type swapped. `DrawdownDuration` claimed
`uint32` values, which was wrong before and after — the array is float64 and
holds whole bar counts. Two more carried NumPy's `dtype` vocabulary. The
profile pages named the third tuple element `bins_ndarray` and `counts_ndarray`;
streaming returns an `array.array('d')` there too.
The one Rust snippet that no longer compiled is in `Data-Layer.md`: it still
matched `r.push(candle)` against an `Option` after the resampler began returning
every bar a push closed. Found by running the repo's own snippet checker, which
nothing runs automatically.
Verified: all 1190 rust snippets compile against the local crate, all 600
`ta.`/`wickra.` references resolve in the installed module, and no page mentions
`ndarray` outside a NumPy input.
The repo's checkers validate that every `ta.Name` resolves, by design — many blocks are schematic, looping over a feed that does not exist. So a snippet can name everything correctly and still be broken, and thirty of them were. Running every block that calls `batch` is what found them. Nineteen Python snippets called the binding with the wrong arity: a close-only series where the indicator takes high, low and close; `ElderRay` and `TDREI` passing one column too many; `VolumeRsi` given volume without close. Three passed a constructor argument the Python binding does not have — `Marubozu`, `SpinningTop` and `Tweezer` keep `with_tolerance` in Rust and deliberately do not carry it across the C ABI, so no binding exposes it — and `KVO` passed three parameters where it takes two. Seven more reached for `.T` on a `Matrix`, which is not an ndarray; `tolist()` is the bridge and the transpose-and-unpack idiom survives around it. One indexed an `array.array` with a boolean mask. Ten JavaScript snippets had the same arity faults, plus two the Python pass could not see: `TDCombo` and `TDSequential` take four constructor arguments in Node, where Python defaults them to the classic 4/9/2/13. `ElderRay` also fed `update` four arguments where it takes three, which made a candle whose high sat below its low. Where an input changed, the printed result was re-checked against the real output rather than assumed. Two expectations were already wrong: `TDREI` documented a warmup of 12 against an actual 11, and `TimeBasedStop` showed a NumPy repr for a value that is no longer a NumPy array. Verified: 405 Python and 217 JavaScript batch snippets run clean, all 1190 Rust snippets compile, and both name checkers pass.
The name checkers validate that every `ta.<Name>` reference exists and stop there by design, because many blocks are schematic. That gap is where the last two commits' defects lived: a snippet can resolve every symbol and still raise the moment a reader runs it. Twenty-five more turned up by executing them. Four derivatives indicators were handed the whole eleven-field tick when each takes only the fields it uses — `EstimatedLeverageRatio` wants open interest and the two sizes, not funding rate and mark price. Two bar builders were fed a single price where `update` takes open, high, low and close. `MIDPRICE` got its close first, which made a candle whose high sat under its low, and `TDTrap` got three arguments where it takes four. Each fix keeps the documented result: the arguments were already there, in the wrong shape, so the printed values still hold and were re-checked against the real output rather than assumed. The two runners are the durable half. A block that fails on an undefined identifier, a missing module or an elided `...` is schematic and does not count; anything else is a snippet that raises in a reader's hands. Both run in CI beside the name checkers, and both were verified to fail by injecting a wrong-arity call.
The sweep that replaced the NumPy return type matched a bare `ndarray` sitting
inside an existing code span on the RangeBars page, and substituted a
backticked `Matrix` into the middle of it:
`batch(closes) -> `Matrix` (k, 3)`
The lookbehind that was meant to keep bare matches out of code spans only looks
at the character immediately before, and here the span's opening backtick was
half a line away. The span now reads `batch(closes) -> Matrix (k, 3)`.
markdownlint caught it — two MD038 errors on the one line — and the docs CI runs
it, so this would have failed there. It is the only instance: the whole corpus
lints clean now, and `vitepress build` renders all 536 pages, which is what
validates the internal links across everything the sweep touched.
kingchenc
marked this pull request as ready for review
August 25, 2026 10:42
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The documentation half of the audit remediation in wickra-lib/wickra#393.
Held for the release. These pages describe the API that PR #393 ships, so
merging them before that lands would put the docs ahead of the registries. Draft
until then.
What changed
299 of the 514 deep dives promised a
numpy.ndarray.batchhas notreturned one since the NumPy dependency was dropped: a scalar indicator returns
array.array('d')and every shape with more than one value per bar returns aMatrix. A reader following the page reached for.shapeor a boolean mask on atype with neither. The right replacement differs per indicator, so it came from
the manifest rather than the prose — 253 scalar, 39 multi-output, 6 profile, 1
bar builder — and the qualifiers that stopped being true moved with the noun, so
"a 1-D
float64np.ndarray" reads as "anarray.array('d')" rather thanstranding
float64on a type with no dtype.Thirty snippets raised the moment a reader ran them. The existing checkers
validate that every
ta.<Name>resolves and deliberately stop there, because manyblocks are schematic. That gap is where these lived: four derivatives indicators
were handed the whole eleven-field tick when each takes only the fields it uses,
two bar builders were fed one price where
updatetakes four,MIDPRICEgot itsclose first and so built a candle whose high sat under its low, seven reached for
.Ton aMatrix, and one indexed anarray.arraywith a boolean mask. Everyfix keeps the documented result — the arguments were already there in the wrong
shape — and each printed value was re-checked against the real output.
Two expectations were wrong before this:
TDREIdocumented a warmup of 12against an actual 11, and
TimeBasedStopshowed a NumPy repr for a value that isno longer a NumPy array.
scripts/run_doc_snippets.pyandscripts/run-doc-snippets.mjsnow executeevery block in CI beside the name checkers, so the next API change cannot break
the docs silently. A block failing on an undefined identifier, a missing module
or an elided
...is schematic and does not count; anything else fails thebuild. Both were verified to fail by injecting a wrong-arity call.
CI
All eight checks pass, including
rust-snippets— which I expected to fail, andwas wrong about. That job checks out
wickra-lib/wickrawithout aref, so itcompiles against
main, whereResampler::pushstill returnsOption<Candle>rather than the
Vec<Candle>that #393 ships. The snippet survives both becauseOption<T>implementsIntoIterator:for closed in r.push(candle?)?iterateszero-or-one items against
mainand every closed bar against #393. The fix issource-compatible with both signatures, which is a better outcome than the one I
predicted.
Verified locally against #393's build: markdownlint clean across 536 files,
vitepress buildrenders all of them, 594 Python and 526 JavaScript snippets runclean, and all 1190 Rust snippets compile.