The 0.2.0 release left wickra@npm stuck at 0.1.4 and never created a
GitHub Release entry because the brand-new `wickra-win32-arm64-msvc`
sub-package name was caught by npm's spam-detection filter on its first
publish attempt (same situation that affected `wickra-win32-x64-msvc`
through 0.1.4 until npm Support unblocked it). A support ticket is open;
until it is resolved, ship 0.2.1 for the five platforms whose
sub-packages are already on npm and re-add Windows ARM64 in a follow-up
release.
Changes for this cycle:
- bindings/node/package.json: remove "wickra-win32-arm64-msvc" from
optionalDependencies and "aarch64-pc-windows-msvc" from
napi.triples.additional.
- bindings/node/npm/win32-arm64-msvc/: removed (will be restored fresh
once the npm name is unblocked).
- .github/workflows/release.yml: comment out the
aarch64-pc-windows-msvc entry of the node-build matrix with a
TODO/restore note.
- Bump every workspace and binding version to 0.2.1 (Cargo.toml,
pyproject.toml, bindings/node/package.json, five npm/<target>
templates, the wiki version table). Cargo.lock regenerated.
- CHANGELOG: new [0.2.1] block consolidating every fix that has landed
on main since 0.2.0 (HV epsilon, examples CI step, fuzz cargo-fuzz
install, MSRV 1.85 -> 1.86 / 1.77 -> 1.88, criterion 0.5 -> 0.8,
tokio-tungstenite 0.24 -> 0.29, tick_aggregator gap-fill cap, every
GitHub Action SHA-pin bump). Compare-link added.
The arm64 loader branch in bindings/node/index.js is left untouched: a
Windows ARM64 user installing 0.2.1 will get the standard
`Cannot find module 'wickra-win32-arm64-msvc'` error from the loader,
which is accurate. PyPI's win-arm64 wheel is unaffected.
Verified locally:
cargo fmt/clippy/test --workspace --all-features -> 630 passed / 0 failed
cargo build -p wickra-examples --bins -> clean
cargo build -p wickra-node -> clean
criterion 0.8.2 raised its MSRV from 1.85 to 1.86 (rustc 1.85.1 is now
explicitly rejected by its Cargo.toml `rust-version`). One more notch
on the same upward drift that already took us from 1.75 -> 1.80 (rayon)
-> 1.85 (clap_lex/edition2024).
Updated:
- Cargo.toml workspace rust-version 1.85 -> 1.86
- .github/workflows/ci.yml MSRV matrix name + toolchain
- ci.yml comment refreshed to reflect the new driving dep
Dependabot opened #13 to bump tokio-tungstenite from 0.24 to 0.29 but
the bare-version bump fails to compile: WebSocketConfig became
#[non_exhaustive] starting with 0.27, so the existing struct-literal
construction
let ws_config = WebSocketConfig {
max_message_size: Some(MAX_MESSAGE_SIZE),
max_frame_size: Some(MAX_FRAME_SIZE),
..WebSocketConfig::default()
};
produces
error[E0639]: cannot create non-exhaustive struct using struct expression
Switch to the builder-style setters that 0.29 exposes on the default
value. Semantics are unchanged; both fields still carry the
MAX_MESSAGE_SIZE / MAX_FRAME_SIZE caps from the original config and the
rest of the WebSocketConfig defaults are preserved by starting from
WebSocketConfig::default().
This supersedes #13 — same target version, plus the code change
Dependabot can't make on its own.
Verified locally:
cargo check -p wickra-data --features live-binance # clean
cargo test --workspace --all-features # 630 passed / 0 failed
cargo clippy --workspace --all-targets --all-features -- -D warnings
Dependabot opened #10 to bump criterion from 0.5.1 to 0.8.2 but the
straight version bump fails to build: criterion::black_box was
deprecated in 0.6 and removed/marked deny-warn in 0.8, so the existing
`use criterion::{black_box, ...}` produces
error: use of deprecated function `criterion::black_box`: use
`std::hint::black_box()` instead
across every bench callsite. Switch the import to std::hint::black_box
(stable since Rust 1.66, well under our MSRV of 1.85) and drop the
criterion re-export.
This supersedes #10 — same target version, plus the code change
Dependabot can't make on its own.
Verified locally:
cargo bench -p wickra --no-run # builds clean
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo test --workspace # 630 passed / 0 failed
The tick_aggregator fuzz target found that TickAggregator::fill_between
allocates one placeholder Candle per skipped bucket without bounding the
gap size. An adversarial input (a clock-glitch tick years in the future)
produced an OOM on libFuzzer after malloc(~3 GB):
SUMMARY: libFuzzer: out-of-memory (malloc(3221225472))
A real-world failure mode too: a single bad timestamp from a flaky feed
could OOM the host process even though every individual tick passed
Tick::new validation.
Fix:
- Compute the gap size up-front via saturating arithmetic, before any
allocation, and refuse with Error::Malformed when it exceeds the new
MAX_GAP_FILL_CANDLES = 1_000_000 cap (≈ 1.9 years of contiguous
one-minute bars, well above any realistic missing-data window).
- Reserve the right Vec capacity in advance once we know the gap fits,
avoiding intermediate reallocations.
- Add two regression tests: gap_fill_rejects_runaway_timestamp_jump
(the fuzz scenario) and gap_fill_at_the_cap_succeeds (exact-cap input
still works).
The cap is exposed as pub const so callers can pre-validate their input
without relying on the error string.
Two unrelated newer-toolchain breakages bundled because they hit on the
same CI run and have the same shape (newer Rust got stricter about
patterns we used):
1. clippy 1.95 added the manual_midpoint lint which fires on every
instance of (a + b) / 2.0 with a help suggesting f64::midpoint.
CI runs with -D warnings so it became a hard error. Twelve sites
were affected — three real call sites in src/ohlcv.rs (median_price),
src/indicators/donchian.rs (DonchianOutput.middle),
src/indicators/ease_of_movement.rs (mid), and
src/indicators/super_trend.rs (hl2); plus eight test-helper
Candle::new constructions across accelerator_oscillator,
atr_trailing_stop, chaikin_volatility, chandelier_exit,
chande_kroll_stop, choppiness_index, super_trend, true_range.
All twelve switched to f64::midpoint (stable since Rust 1.85,
our workspace MSRV).
2. usize::is_multiple_of is still unstable (rust-lang/rust#128101) and
only stabilizes in Rust 1.87, but the MSRV CI job uses 1.85. The
two call sites in bollinger.rs and sma.rs (added with the R7
periodic-reseed tests) switched back to i % 2 == 0.
The first MSRV bump to 1.80 fixed the rayon-core floor but ran into a
second transitive-dep floor on the same CI run:
error: failed to parse manifest at clap_lex-1.1.0/Cargo.toml
Caused by: feature `edition2024` is required
The package requires the Cargo feature called `edition2024`, but
that feature is not stabilized in this version of Cargo (1.80.1).
clap_lex 1.1.0 is pulled in by criterion (dev-dep on the wickra crate)
via clap 4.6 -> clap_builder 4.6 -> clap_lex 1.1. edition2024 was
stabilized in Rust 1.85, so lift the workspace MSRV one more step. The
1.85 floor subsumes the rayon-core 1.80 requirement; bindings/node
stays at 1.88 (napi-build) which already covers everything below it.
Also fix the fuzz job: cargo-fuzz defaulted to building for
x86_64-unknown-linux-musl, which is not installed on the GitHub-hosted
ubuntu runner. Pass --target x86_64-unknown-linux-gnu explicitly on
every cargo fuzz run invocation so it builds for the actual host
target.
The previous MSRV pins were below what current transitive dependencies
need:
- rayon-core 1.13.0 (pulled in by the optional `parallel` feature on
wickra-core via rayon) requires rustc >= 1.80; under the old 1.75
workspace MSRV the CI MSRV job broke with "package `rayon-core
v1.13.0` cannot be built because it requires rustc 1.80 or newer".
- napi-build 2.3.2 (the build-script crate that napi-derive 2.x calls
into) requires rustc >= 1.88; under the old 1.77 node-binding MSRV
the CI MSRV-node job broke with "package `napi-build v2.3.2` cannot
be built because it requires rustc 1.88 or newer".
Pinning the deps backwards would have frozen us out of upstream
security/fix releases for both crates. Lifting the MSRV is the cleaner
path for a young 0.x library — downstream consumers on older toolchains
can stay on the already-published 0.2.0.
Updated:
- Cargo.toml workspace rust-version 1.75 -> 1.80
- bindings/node/Cargo.toml rust-version 1.77 -> 1.88
- .github/workflows/ci.yml MSRV matrix names + toolchain values + comment
- CHANGELOG.md [Unreleased] documents the bump
The previous `cargo install cargo-fuzz --locked` resolved against
cargo-fuzz's own Cargo.lock which pins to rustix 0.36.5. That rustix
version still annotates its source with internal #[rustc_*] attributes,
and the current nightly compiler rejects those attributes from
out-of-tree crates, so cargo-fuzz never finished compiling on CI:
error: attributes starting with `rustc` are reserved for use by the
`rustc` compiler
--> rustix-0.36.5/src/backend/linux_raw/io/errno.rs:28
error: could not compile `rustix` (lib) due to 4 previous errors
error: failed to compile `cargo-fuzz v0.13.1`
Switch the install to taiki-e/install-action, the same prebuilt-binary
provider we already use for cargo-llvm-cov. That skips the full
transitive-dependency compile and lets the fuzz-smoke job actually run.
The Z5 reorganisation moved every runnable example out of the per-crate
examples/ folders and into a dedicated wickra-examples crate at
examples/rust/, with the binaries living under src/bin/<name>.rs. The
old ci.yml Compile-examples step still pointed at the now-deleted
cargo example targets backtest (wickra) and live_binance (wickra-data),
which is why Rust windows-latest failed with 'no example target named
backtest in wickra package' and 'no example target named live_binance
in wickra-data package'.
Replace both calls with a single cargo build -p wickra-examples --bins.
That covers backtest, live_binance, fetch_btcusdt, multi_timeframe,
parallel_assets and streaming in one shot, and the wickra-examples
crate already enables the live-binance feature on its wickra-data dep
so no extra --features flag is needed.
The mathematical result of HistoricalVolatility on a perfectly geometric
price series is exactly zero — but the underlying 1.01_f64.powi(i) +
log-return + std-dev cascade accumulates platform-sensitive FP drift on
the order of 1e-7 on x86_64 Linux and macOS (the Windows result happened
to round closer to zero, which is why the test passed locally and on the
Windows CI runner but failed on Linux and macOS).
Bump the tolerance from 1e-9 to 1e-6. That stays four decimal places
below any realistic annualised volatility value while comfortably
absorbing the observed cross-platform drift.
Also extend the comment to document the rationale so the next person
who reads the test does not tighten it back down.
This release carries the full post-audit work — 46 new indicators
(25 → 71), an eight-family taxonomy restructure, new bindings for
RollingVWAP, the WASM streaming-update parity, the pyo3/numpy CVE
fix, the SMA/Bollinger drift bound, the O(1) LinearRegression
refactor, the UlcerIndex deque and the PSAR is_ready/reset fixes
plus a refreshed example suite and wiki. The earlier 0.1.5 number
was never published; jumping straight to 0.2.0 is the cleaner signal
for the scope of the change.
Bumped:
- Cargo.toml workspace + wickra-core workspace-dep version
- bindings/python/pyproject.toml
- bindings/node/package.json + optionalDependencies (six platform pins)
- 6 x bindings/node/npm/<target>/package.json
- Cargo.lock regenerated
- CHANGELOG.md [0.2.0] header + compare-link
- docs/wiki/Home.md published-versions table
- docs/wiki/Quickstart-{Rust,Node,WASM}.md + Warmup-Periods.md
version-pinned narrative lines
Verified locally:
- cargo fmt/clippy/test (628 passed, 0 failed)
- cargo deny check (no suppression)
- bindings/node node --test (92/92)
- bindings/python pytest (118/118)
- import wickra reports 0.2.0 with 72 indicator classes
Replace the TBD placeholder under ## [0.1.5] with today's date so the
release header is complete. The compare-link at v0.1.4...v0.1.5 is
already in place and points at the right pair.
Every other manifest in the repo carries the author string
kingchenc <kingchencp@gmail.com> — workspace Cargo.toml,
bindings/node/package.json, all per-platform npm package.jsons,
release.yml. pyproject.toml had neither authors nor maintainers, so
the PyPI listing fell back to the implicit empty value. Align with
the rest of the repo.
Indicators-Overview.md listed RollingVwap as its own row in the Volume
table (count = 9, total = 72), but Home.md and README.md treat it as
a sub-variant of Vwap (count = 8, total = 71). Drop the standalone row
in Indicators-Overview and fold the rolling variant into the Vwap row.
Warmup-Periods.md keeps both constructors (Vwap::new() and
RollingVwap::new(n) have different warmup periods, which is the whole
purpose of that page) but adds a note explaining that the row count
exceeds the 71 canonical indicators by one.
The module doc-comment in bindings/node/src/lib.rs still pointed at
@wickra/wickra. The package is published as bare "wickra" — every
README, Quickstart and example already uses that name. Align the
inline doc with the published name.
The Quickstart-Node API-surface table claimed `warmupPeriod()` was
"not exposed on every multi-output class". After the B-series fixes
in `todo-detailed.md` and the R3/R8 pass on this branch, every Node
indicator class — single- and multi-output, scalar- and candle-input —
exposes `warmupPeriod()`. Updated the note accordingly.
The Quickstart-WASM page only mentioned `MACD` and `BollingerBands` as
multi-output indicators. After R3 (this branch) every candle-input
WASM class also exposes a structured `update`: `Stochastic`, `ADX`,
`Keltner`, `Donchian`, `Aroon` (plus `SuperTrend`, which has always
been there).
The "Multi-output indicators" section now lists every multi-output
shape in a table, and a short note at the end calls out the 0.1.5
parity: every candle-input indicator ships `update` / `batch` /
`reset` / `isReady` / `warmupPeriod`, so browser code doesn't need
to replay `batch` on each tick anymore.
Three indicator pages get a short follow-up paragraph that surfaces an
internal implementation detail the audit findings made user-visible:
- `Indicator-LinearRegression.md` gains a "Complexity" section explaining
the O(1) update (precomputed `Σx`, `Σxx`; incrementally slid `Σy`,
`Σxy` via the closed-form sliding identity), and the existing
"Reset" bullet mentions the additional running accumulators. The same
story applies to `LinRegSlope` and `LinRegAngle` (the page now links
to both rather than repeating the derivation three times).
- `Indicator-Sma.md` and `Indicator-BollingerBands.md` mention the
periodic reseed (`16 · period` updates) that caps floating-point
drift on long-running streams. Amortised cost is still O(1) and the
user-facing behaviour on benign inputs is unchanged.
No behavioural claim, no API claim, no example changes — just narrative
catching up with the implementation.
Versions bumped to 0.1.5 in every authoritative location:
- workspace `Cargo.toml` (`[workspace.package].version`, the
`wickra-core` path dependency pin).
- `bindings/python/pyproject.toml`.
- `bindings/node/package.json` (main + all six `optionalDependencies`
pins).
- All six per-platform `bindings/node/npm/<target>/package.json`
templates.
CHANGELOG: the accumulated `[Unreleased]` block is promoted to
`[0.1.5] - TBD` (date left for the user to set at tag time); the new
`[Unreleased]` header sits empty above it; the compare link table is
extended with `[0.1.5]: …compare/v0.1.4...v0.1.5` and the
`[Unreleased]` link is repointed to `…compare/v0.1.5...HEAD`.
Wiki refresh for 0.1.5 (R20 + Z2):
- `Home.md` version pin table updated; the Quickstart-Node hint replaces
the "spam filter holding back Windows" caveat with "0.1.5 is the
first release in which `npm install wickra` works end-to-end on
Windows" (npm Support released the name on 2026-05-22).
- `Quickstart-Node.md`'s Windows caveat is rewritten to explain the
history (`0.1.1`–`0.1.4` of `wickra-win32-x64-msvc` are burned) and
the resolution (0.1.5+ installs cleanly).
- `Quickstart-Rust.md` version mention bumped.
- `Warmup-Periods.md` note bumped + corrected: every Node and WASM
class — single- and multi-output — now exposes `warmupPeriod()` after
R3 (this branch), not only the single-output ones.
`release.yml` `publish_dir` no longer silently swallows a
second-attempt platform-package publish failure with a `::warning::`
and `return 0`. A real failure (after the existing 30s retry) now
emits an `::error::` and fails the job. The original mask is exactly
what allowed the `wickra-win32-x64-msvc@0.1.1–0.1.4` spam-filter
rejections to land four times in a row without anyone noticing (audit
finding R20). Failing loud means the next regression of this shape is
caught at the release run, not by a Windows user trying to
`require('wickra')`.
This commit does NOT push, tag, or trigger a release — the user
publishes the 0.1.5 tag themselves once the manual npm-republish
smoke test confirms `wickra-win32-x64-msvc@0.1.5` accepts publish on
the freshly-released name.
PSAR — the wiki page still claimed "Node streaming. Not exposed in the
Node binding." That was true for an early release but is no longer:
the Node binding has exposed `psar.update(high, low, close)` since the
B1 fix in todo-detailed.md, and the WASM binding now exposes the same
streaming surface (audit finding R3, this branch). The page lists all
three streaming + batch shapes and adds a paragraph on the new
`is_ready` convention (audit finding R6 — flips on the first non-None
SAR, not on the seed candle).
HistoricalVolatility — the "Non-positive prices" edge-case note still
described the old behaviour ("that return is treated as 0"). The new
behaviour skips the bad tick entirely (audit finding R13): the
indicator's state is left untouched, the previous valid value is
returned, and the next real tick re-anchors against the previous
*valid* price. Updated the note to describe the new behaviour and
explain why (silently treating bad ticks as "no movement" underreports
realised volatility).
R10 — `cross-library-bench` previously ran on every push and every PR
to `main`, adding 5–10 minutes of build + bench time per CI run with
no automated consumer of the artefact. It moves to a dedicated
workflow (`.github/workflows/bench.yml`) that fires nightly at 03:00
UTC and on-demand via `workflow_dispatch` (with optional `size` /
`iterations` inputs). The job in `ci.yml` is removed and a pointer
comment is left in its place so future readers find the new home.
R11 — `pyproject.toml`'s `requires-python = ">=3.9"` already allowed
Python 3.13 installs, but the classifier list stopped at 3.12 and CI
tested only 3.9 / 3.11 / 3.12. The Python CI matrix gains `"3.13"`
and the matching `Programming Language :: Python :: 3.13` classifier
is added so PyPI listings and version-search tooling reflect the
actually-tested range.
R15 — `bindings/node/package.json` and all six per-platform subpackage
templates (`npm/{darwin-arm64,darwin-x64,linux-arm64-gnu,linux-x64-gnu,
win32-arm64-msvc,win32-x64-msvc}/package.json`) plus the WASM
`release.yml` enrich step switch the `license` field from the npm
convention `SEE LICENSE IN LICENSE` to the SPDX identifier
`PolyForm-Noncommercial-1.0.0`. npm's license search and downstream
tooling can now surface the actual license.
R18 — every `engines.node` field is bumped from `>= 16` to `>= 18`.
The package's test script (`node --test __tests__/`) uses the built-in
`node --test` runner that landed in Node 18; advertising support for
Node 16 / 17 was a guarantee we never verified (CI tests on Node 18 /
20 only) and would have produced a confusing error on those versions.
R19 — README's benchmark section gains an explicit hardware /
software-version block and reframes the absolute µs values as a
relative-speedup snapshot rather than a universal contract. Also tells
the reader how to reproduce locally (`pip install -e
bindings/python[bench]` + `python -m benchmarks.compare_libraries`) and
where the CI artefact lives.
Side effects:
- R16 / R17 (built wheel under `bindings/python/dist/` and
`examples/node/node_modules/`) — verified that both are already
covered by `.gitignore` (`*.whl`, `dist/`, `**/node_modules/`) and
were never tracked by git. The local directories have been cleared;
no `.gitignore` change needed and no committed file removed.
R13 — `HistoricalVolatility::update` previously substituted `0.0` for
the log-return whenever `prev <= 0` or `input <= 0`. The log-return is
undefined there, and silently treating bad ticks as "no movement"
underreports realised volatility on broken data feeds. The fix skips
non-positive prices entirely: `self.last` is returned, state is left
untouched, and the next real tick re-anchors against the previous
*valid* `prev_price`. This matches how every other indicator handles
invalid inputs (SMA / EMA / ROC / Bollinger).
A new test `skips_non_positive_prices` proves the invariant: after a
warmed-up indicator, two consecutive bad ticks (`-5.0` and `0.0`) must
return the baseline value, and a subsequent real positive tick must
produce the same output as a control indicator that simply never saw
the bad ticks.
R14 — `Tick::new` previously returned `Error::InvalidCandle` for
negative volume. A tick is not a candle; downstream tick-stream
pipelines should be able to match on a semantically-correct error. A
new `Error::InvalidTick { message }` variant is added; the existing
test is updated to assert against it. Python's `map_err` is extended
to forward the new variant as `PyValueError`; the Node and WASM
bindings format via `Error::to_string()` and pick the new variant up
automatically without source changes.
`Sma` and `BollingerBands` both maintained their running `sum` (and
`sum_sq` for Bollinger) with a single-subtract incremental update. That
is correct in exact arithmetic, but in f64 the sequence `sum -= old;
sum += new` on long streams with alternating large/small magnitudes
can accumulate catastrophic-cancellation error. Bollinger's existing
`.max(0.0)` clamp on the computed variance was a band-aid for the same
root cause — the drift had already driven the running variance below
zero.
The fix: every `16 · period` finite updates, reseed `sum` (and `sum_sq`
for Bollinger) from the live window. Amortised cost stays at O(1) —
`O(period)` work amortised over `O(period)` updates — and the reseed
strategy is named after the constant `RECOMPUTE_EVERY` so the
intention is clear at the call site.
Behaviour is unchanged on inputs that did not drift to begin with
(every existing test still passes, including `batch_equals_streaming`
and the SMA proptest). Two new stress tests
(`long_stream_drift_stays_bounded` in each module) feed a
magnitude-alternating stream for `5 · RECOMPUTE_EVERY · period`
updates and assert the reported value tracks a fresh from-scratch
computation over the live window to within tight tolerance — these
would have failed without the reseed on Bollinger's `sum_sq`.
The misleading `sma.rs` comment that claimed drift was already
bounded by recomputing the sum after each pop is rewritten to
describe the actual reseed strategy (audit finding L2-Rust).
Follow-up to b340ecd — the doc comment on
`warmup_period_matches_first_some_for_every_parameter_set` had an
unbalanced inline-code span ("`Some``") that tripped
`clippy::doc_invalid_doc_attributes` (caught by `clippy -D warnings`
but not by `cargo build` or `cargo test`). Rephrased the sentence so
every backtick is paired. No code change, no test change.
Audit finding R12 claimed `Coppock::warmup_period()` was off by one
because it returns `max(roc_long, roc_short) + wma`, while
`Roc::warmup_period() = period + 1`. After tracing the actual emission
sequence the existing formula is correct: when both ROCs reach `Some`
at 0-based index L (the slower of `roc_long_period` and
`roc_short_period`), the WMA receives its first input there and emits
its `wma_period`-th value at 0-based index `L + wma_period − 1`. The
`warmup_period()` is the 1-based count of inputs needed before the
first `Some`, i.e. `L + wma_period`. R12 was a misread by both Sonnet
audit agents and the Opus verifier — none of them traced the actual
emission timeline.
This commit:
- Expands the doc comment on `warmup_period` with the precise emission
argument and a worked example for `Coppock::new(6, 4, 3)` (the
existing test) so a future reader cannot mis-derive the formula.
- Adds `warmup_period_matches_first_some_for_every_parameter_set`,
which asserts `out[warmup - 1].is_some()` for five parameter
combinations — including the audit's smoking gun `(4, 2, 3)`. The
audit's proposed `max + 1 + wma` formula would have predicted index
7 (the 8th input) for that combination; the real first `Some` lands
at index 6 (the 7th input), exactly what the current formula
reports.
No behaviour change — the audit was wrong and the test makes the
contract regression-proof.
`LinearRegression::fit` and `LinRegSlope::update` previously iterated the
full `period`-window on every tick to recompute `Σy` and `Σxy` from
scratch — O(period) per update, in violation of the `Indicator` trait's
O(1) contract. `LinRegAngle` inherits the cost transitively because it
delegates to `LinRegSlope`.
This commit slides the OLS state in closed form. The constant terms
(`Σx`, `Σxx`, the denominator `n·Σxx − (Σx)²`) were already precomputed
in `new`. The new running state is:
- `sum_y: f64` — running sum of the values currently in the window.
- `sum_xy: f64` — running Σ(x · y) where `x` is the position of each
value inside the trailing window (`0` for the oldest, `n−1` for the
newest).
On every push, when the window is already full the front value `y₀` is
popped and the indices of every remaining value shift down by 1; the
identity
new_Σxy = old_Σxy − old_Σy + y₀
closes the slide in O(1). The new value is then pushed at position `k`
(the current length before the push), contributing `k · new_value` to
`sum_xy` and `new_value` to `sum_y`. The output is the same TA-Lib OLS
formula evaluated against the incremental accumulators.
Behaviour is unchanged: same per-tick values, same warmup, same NaN
semantics. Two new tests compare the O(1) result bar-by-bar against a
fresh O(n) refit on a noisy ramp (sliding-phase dominated), a step
function (large pop/push deltas), and constants (tests floating-point
drift) — agreement is within `1e-9`.
`LinRegAngle` benefits automatically through its `LinRegSlope` field.