Skip to content

Docs remediation: the NumPy return type, and snippets that raise when run - #101

Merged
kingchenc merged 6 commits into
mainfrom
audit/docs-remediation
Aug 25, 2026
Merged

Docs remediation: the NumPy return type, and snippets that raise when run#101
kingchenc merged 6 commits into
mainfrom
audit/docs-remediation

Conversation

@kingchenc

@kingchenc kingchenc commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

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. batch has not
returned 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 a
Matrix. A reader following the page reached for .shape or a boolean mask on a
type 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 float64 np.ndarray" reads as "an array.array('d')" rather than
stranding float64 on 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 many
blocks 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 update takes four, MIDPRICE got its
close first and so built a candle whose high sat under its low, seven reached for
.T on a Matrix, and one indexed an array.array with a boolean mask. Every
fix 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: 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.

scripts/run_doc_snippets.py and scripts/run-doc-snippets.mjs now execute
every 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 the
build. Both were verified to fail by injecting a wrong-arity call.

CI

All eight checks pass, including rust-snippets — which I expected to fail, and
was wrong about. That job checks out wickra-lib/wickra without a ref, so it
compiles against main, where Resampler::push still returns Option<Candle>
rather than the Vec<Candle> that #393 ships. The snippet survives both because
Option<T> implements IntoIterator: for closed in r.push(candle?)? iterates
zero-or-one items against main and every closed bar against #393. The fix is
source-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 build renders all of them, 594 Python and 526 JavaScript snippets run
clean, and all 1190 Rust snippets compile.

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.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 25, 2026

Copy link
Copy Markdown

Deploying wickra-docs with  Cloudflare Pages  Cloudflare Pages

Latest commit: 2cc3553
Status:⚡️  Build in progress...

View logs

@kingchenc
kingchenc marked this pull request as ready for review August 25, 2026 10:42
@kingchenc
kingchenc merged commit dc6b137 into main Aug 25, 2026
6 of 7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant