* ci: fix webpage count sync crashing on removed public/hero.svg
The webpage indicator-count sync sed'd index.md, .vitepress/config.ts and
public/hero.svg, but hero.svg was removed from wickra-lib/webpage (the count is
now baked into og-banner.webp at build time from index.md). Under 'bash -e' the
missing file made sed exit 2 before the commit/push, so the count never reached
the webpage repo and its OG banner stayed stale — masked green by the step's
continue-on-error. Drops public/hero.svg from the sed and git add; index.md and
.vitepress/config.ts (both still carry the count) remain.
* docs: point README banner at the self-updating org profile image
Switches the top README banner from https://wickra.org/og-banner.webp (baked at
webpage deploy time) to the org profile banner that wickra-lib/.github's
banner.yml regenerates from the indicator count on every sync
(raw.githubusercontent.com/wickra-lib/.github/main/profile/wickra-banner.webp).
The ?v=227 query busts GitHub's Camo image cache; the next commit teaches
sync-about to bump it with the count.
* ci: bump README banner cache-buster alongside the indicator count
Extends the PR-head README counter patch to also rewrite wickra-banner.webp?v=N
to the current count. The banner now points at the org profile image, whose
content changes when .github/banner.yml regenerates it; bumping the ?v query
busts GitHub's Camo cache so the README shows the new banner immediately. Rides
in the existing count-sync commit, so no extra commit lands on main.
* docs: changelog entry for self-updating README banner and sync fix
* ci: use npm ci instead of npm install for reproducible installs
Pins the node binding dependency install to the committed package-lock.json
integrity hashes (OpenSSF Scorecard PinnedDependencies). npm ci installs
strictly from the lockfile; npm install could resolve newer patch versions.
Covers ci.yml and both release.yml node steps.
* ci: hash-pin Python dev tooling in ci.yml (Scorecard #19)
Replaces the unpinned 'pip install maturin pytest numpy hypothesis' with a
hash-locked '--require-hashes -r' install (OpenSSF Scorecard PinnedDependencies).
Two lock files are needed because numpy publishes no single release with wheels
for both cp39 and cp313 (<=2.0.2 has cp39 only, >=2.1 drops cp39):
ci-dev-py39.txt numpy 2.0.2 (Python 3.9, + tomli/exceptiongroup)
ci-dev-py3.txt numpy 2.4.6 (Python 3.10+)
The step selects the file by matrix.python-version under shell: bash. Both are
generated from ci-dev.in via uv (scripts/update-lockfiles.sh, added next).
* ci: hash-pin Python deps in bench.yml (Scorecard #16)
Replaces the unpinned 'pip install maturin numpy pandas talipp finta' with a
hash-locked '--require-hashes -r .github/requirements/bench.txt' install.
bench.yml runs on a single Python version (3.11), so one lock file (generated
from bench.in via uv) is sufficient.
* build: add scripts/update-lockfiles.sh to regenerate all lockfiles
One command refreshes every committed lockfile across languages: Cargo.lock and
fuzz/Cargo.lock (cargo update), the Node binding package-lock.json, and the
hash-pinned Python requirements under .github/requirements/ (uv pip compile
--generate-hashes). Uses uv for the Python locks so a target Python version's
hashed transitive closure can be resolved without that interpreter installed
(needed for the numpy cp39/cp313 split); bootstraps uv if absent.
.gitattributes pins *.sh to LF so the script stays runnable on Linux/macOS.
* ci: split ci-dev requirements per Python version + Dependabot rehash
Splits the single ci-dev.in into ci-dev-py39.in (numpy <2.1, the last series
with cp39 wheels) and ci-dev-py3.in (3.10+), giving a 1:1 .in->.txt layout.
The cap keeps Python 3.9 permanently installable and stops Dependabot from
proposing 3.9-breaking numpy bumps.
Adds a Dependabot pip entry on /.github/requirements so the hash-locked tooling
is kept current automatically; the canonical manual refresh stays
scripts/update-lockfiles.sh. Only the '# via -r' provenance lines in the .txt
change; no package versions or hashes move.
* ci: cache pip and npm downloads in the PR-loop jobs
Adds setup-python cache: pip (ci.yml python matrix + bench.yml, keyed on the
hash-locked requirements) and setup-node cache: npm (ci.yml node job, keyed on
bindings/node/package-lock.json), on both the primary and retry setup steps.
Scoped to jobs that actually install dependencies; the examples-smoke and
clippy-bindings jobs install nothing and are left uncached. release.yml is
intentionally left out: it runs only on tag push (not the PR loop) and is the
publish-critical path, so no caching is added there.
* docs: document hash-pinned requirements and update-lockfiles.sh
Updates the lockfile-policy table: the bindings/python row no longer claims CI
installs tooling unpinned, and a new .github/requirements row documents the
hash-locked CI/bench tooling and the per-Python-version ci-dev split. Adds a
paragraph pointing contributors at scripts/update-lockfiles.sh (uv-based,
self-bootstrapping) as the canonical lockfile refresh.
* docs: changelog entry for hash-pinned CI dependency installs
* feat(core): add 3 trade-flow microstructure indicators
SignedVolume (per-trade size signed by aggressor), CumulativeVolumeDelta
(running signed-volume total), and TradeImbalance (rolling buy/sell volume
imbalance over a trade window). All consume the Trade type, with full unit
coverage. Extends the Microstructure family.
* feat(bindings): expose trade-flow microstructure indicators
Python, Node and WASM bindings for SignedVolume, CumulativeVolumeDelta and
TradeImbalance. Each takes a trade via update(price, size, is_buy); Python and
Node expose a batch over three parallel arrays, WASM exposes per-trade update.
Regenerates node index.d.ts/.js.
* test(bindings,fuzz,bench): cover trade-flow microstructure indicators
Python and Node: reference values, streaming-vs-batch, lifecycle/repr and input
validation (zero window, negative size, non-positive price, mismatched batch
lengths). New indicator_update_trade fuzz target. Synthetic trade-tape benches
(signed_volume cheapest, trade_imbalance windowed/expensive).
* docs: add trade-flow indicators + bump counter to 227
README Microstructure family row gains signed volume / CVD / trade imbalance and
the counter goes 224 -> 227; CHANGELOG records the trade-flow indicators.
* feat(core): add microstructure input types (OrderBook, Trade, TradeQuote)
New non-OHLCV value types for the order-book / trade-flow indicator family:
Level, OrderBook (sorted, uncrossed depth snapshot), Side, Trade (with
aggressor side), and TradeQuote (trade paired with prevailing mid). Each has a
validating constructor plus a new_unchecked hot-path constructor, with full
unit coverage. Adds InvalidOrderBook / InvalidTrade error variants.
* feat(core): add 5 order-book microstructure indicators
OrderBookImbalanceTop1/TopN/Full (signed depth imbalance), Microprice
(size-weighted fair value), and QuotedSpread (top-of-book spread in bps). All
consume the OrderBook snapshot type, emit f64, are stateless and ready after
the first snapshot, with full unit coverage. Registers a new Microstructure
family in the taxonomy.
* feat(bindings): expose order-book microstructure indicators
Python, Node, and WASM bindings for OrderBookImbalanceTop1/TopN/Full,
Microprice and QuotedSpread. Each takes a depth snapshot via four equal-length
(bid_px, bid_sz, ask_px, ask_sz) arrays. Python and Node expose a batch over a
list of snapshots; WASM exposes per-snapshot update (the streaming model that
fits a browser book feed). Regenerates node index.d.ts/.js and registers the
new InvalidOrderBook/InvalidTrade arms in the Python error mapping.
* test(bindings,fuzz): cover order-book microstructure indicators
Python: smoke, reference values, streaming-vs-batch, lifecycle/repr and input
validation (mismatched lengths, crossed book, misordered levels, zero levels)
for all five order-book indicators. Node: reference values, streaming-vs-batch,
and rejection cases. Adds an indicator_update_orderbook fuzz target driving
every order-book indicator over arbitrary (incl. degenerate) snapshots.
* bench(microstructure): synthetic order-book benchmarks
Add a bench_orderbook_input harness and synthesise a five-level book around
each candle close (no order-book dataset ships with the repo). Benches the
cheapest (top-of-book imbalance) and most-expensive (full-depth imbalance) plus
microprice, matching the curated cheapest/expensive-per-family approach.
* docs: add Microstructure family + bump indicator counter to 224
README gains the Microstructure family row (order-book imbalance, microprice,
quoted spread) and the indicator counter goes 219 -> 224 across seventeen
families; CHANGELOG records the new order-book indicators and value types.
The LICENSE now carries an Additional Permissions section on top of
PolyForm Noncommercial 1.0.0, so the bare SPDX id no longer describes
it exactly. Update the package manifests to reference the actual file
instead of claiming the unmodified standard:
- Cargo (workspace + all crates): license -> license-file = "LICENSE"
- npm (main + 6 platform packages): LicenseRef-Wickra-Noncommercial-1.0.0
- PyPI: license text notes the additional personal-account permissions
Append an Additional Permissions section to the PolyForm Noncommercial
License 1.0.0. The base PolyForm text is unmodified; the section only
broadens the grant. It explicitly permits a natural person to use the
software for their own personal account, including running a trading
bot on their own capital for profit, matching the README's plain-English
summary (hobby trading bots: all fine). Commercial sale of the software
or of services built around it still requires a commercial license.
* feat(core): add signed dragonfly/gravestone encoding to Doji
Doji gains an opt-in `.signed()` mode that classifies a detected Doji by the
position of its body within the bar range: dragonfly (long lower shadow) emits
+1.0 (bullish), gravestone (long upper shadow) emits -1.0 (bearish), and a
long-legged/standard Doji emits 0.0. The default detection-flag behaviour
(+1.0/0.0) is unchanged, so existing callers are unaffected.
The other 14 candlestick patterns already emit the uniform +1 bull / -1 bear /
0 none convention; document that explicitly with a "Signed +-1 encoding"
section on each so the whole family is a consistent drop-in ML feature.
* feat(bindings): expose Doji signed mode in python, node, wasm
Hand-write the Doji binding in all three language bindings (instead of the
shared candle-pattern macro) so it accepts an opt-in `signed` flag and exposes
an `is_signed`/`isSigned` accessor:
- Python: `Doji(signed=False)` keyword argument
- Node: `new Doji(signed?)` optional constructor argument (index.d.ts/.js
regenerated via napi build)
- WASM: `new Doji(signed?)` optional constructor argument
The default construction is unchanged, so existing callers keep the
direction-less +1/0 detection flag.
* test(bindings,fuzz): cover Doji signed dragonfly/gravestone encoding
- python: dragonfly(+1)/gravestone(-1)/neutral(0) and default-flag cases in
test_known_values
- node: equivalent signed/default assertions in indicators.test.js
- fuzz: drive a signed Doji alongside the default in indicator_update_candle
* docs: document signed candlestick convention and Doji signed mode
README gains a candlestick sign-convention note; CHANGELOG records the new
opt-in Doji signed dragonfly/gravestone encoding under [Unreleased].
Releases the cross-asset / pairwise indicator family (PR #109):
PairwiseBeta, PairSpreadZScore, LeadLagCrossCorrelation, Cointegration,
RelativeStrengthAB. Indicator count 214 -> 219.
Bumps workspace + binding versions and the CHANGELOG ([Unreleased] ->
[0.4.1]) with the new compare URL.
* feat(core): add PairwiseBeta cross-asset indicator
Rolling OLS slope of one asset's log-returns on another's. Unlike Beta,
which regresses the raw inputs it is fed, PairwiseBeta differences
consecutive prices into log-returns internally -- the conventional way to
measure cross-asset beta, where a beta on price levels would be dominated
by the shared trend.
Two-series Indicator<Input = (f64, f64)>, exposed in Rust, Python, Node
and WASM, with unit/known-value/streaming tests and a pair fuzz target.
* feat(core): add PairSpreadZScore cross-asset indicator
Standardised log-spread ln(a) - beta*ln(b) of a pair, where beta is a
rolling-OLS hedge ratio and the spread is z-scored over its own look-back.
The canonical mean-reversion / statistical-arbitrage entry signal, with
independent beta_period and z_period windows.
Two-series Indicator<Input = (f64, f64)>, exposed in Rust, Python, Node
and WASM, with sign/known-value/streaming tests and a pair fuzz target.
* feat(core): add LeadLagCrossCorrelation cross-asset indicator
Reports the integer offset k in [-max_lag, max_lag] that maximises
|corr(a[t], b[t+k])|, answering which of two assets leads the other and by
how many bars. A positive lag means a leads b. Fully causal: a's window is
held centred while b's window slides across the buffered history, so every
lag is evaluated only against data already seen.
Struct output { lag, correlation }, exposed in Rust, Python, Node and WASM
with lead-detection/streaming tests and a pair fuzz driver.
* feat(core): add Cointegration (Engle-Granger + ADF) indicator
Rolling pairs-trading screen: an OLS hedge ratio of a on b, the spread
(residual) a - (alpha + beta*b), and an augmented Dickey-Fuller t-statistic
on the spread with configurable lags. A strongly negative statistic flags a
mean-reverting, tradeable spread. Includes a small Gaussian-elimination
solver for the augmented regression.
Struct output { hedge_ratio, spread, adf_stat }, exposed in Rust, Python,
Node and WASM with stationarity/hedge-ratio/streaming tests and a pair fuzz
driver.
* feat(core): add RelativeStrengthAB cross-asset indicator
Comparative relative strength of two assets: the ratio line a/b together
with its moving average and its RSI, the classic asset-vs-asset /
asset-vs-index rotation screen. Composes the existing Sma and Rsi over the
ratio; a zero denominator or non-finite price is skipped.
Struct output { ratio, ratio_ma, ratio_rsi }, exposed in Rust, Python, Node
and WASM with flat/rising-ratio/streaming tests and a pair fuzz driver.
* test(cointegration): cover ADF guard branches
The ADF helper's short-series and degrees-of-freedom guards and the
zero-dispersion (perfect AR) path are unreachable through the public
Cointegration API (period >= 2*adf_lags + 4), so exercise them with direct
unit tests on adf_no_constant. The second linear solve cannot be singular
once the coefficient solve on the same matrix has succeeded, so it now uses
expect() instead of a dead error branch.
GitHub release immutability locks a release's assets at publish time. The
current flow publishes the release in github-release and only afterwards uploads
the Sigstore provenance bundle (P21.1e) via 'gh release upload', which
immutability would reject (actions/attest-build-provenance#734).
Reorder to draft -> attach everything -> publish:
- github-release now creates the release as a draft (draft: true) with all build
artefacts.
- attestations attaches the provenance bundle to the draft (gh release upload
works on drafts), unchanged otherwise.
- a new publish-release job flips the draft to published + latest, gated on
'always() && needs.github-release.result == success' so a Sigstore hiccup in
attestations costs only the provenance asset, never the release — the same
isolation as before. A skipped github-release (failed publish) skips this too.
Correct with immutability off (today: release ends published with every asset)
and on (later, user toggle: all assets present before the lock). No behaviour
removed; nothing deleted.
* docs(readme): surface the documentation site (docs.wickra.org)
The README never linked the canonical docs at docs.wickra.org — visitors had
no path from the repo to the per-indicator deep dives, quickstarts, and
guides. Add a docs badge, a Documentation section mirroring the binding
READMEs and docs/README.md, and an Indicators-Overview link in the indicator
section.
* ci(sync-about): keep the wiki pointer page's indicator count in sync
The GitHub wiki was collapsed to a single Home.md that points at
docs.wickra.org but still names the indicator count ('… for all N indicators').
Add a count-sync step mirroring the docs/webpage steps — clone wickra.wiki into
its own dir, sed Home.md, commit as wickra-bot, push — with the same
continue-on-error soft-skip so a missing PAT scope never fails the run.
OpenSSF Scorecard's Signed-Releases check scans the GitHub Release *assets* for
signed/provenance files (`*.intoto.jsonl`, `*.sig`, ...). It does not look at
GitHub's separate attestations store, so although the attestations job has signed
the published bytes since v0.4.0, the v0.4.0 release assets carried no provenance
file and the check stayed at 0.
Attach the Sigstore provenance bundle (already produced by
actions/attest-build-provenance) to the release as `wickra-<tag>.provenance.intoto.jsonl`:
- github-release now exposes its resolved tag as a job output.
- attestations `needs: github-release` (so the Release already exists), gains
`contents: write`, gives the attest step an id, and uploads the bundle with
`gh release upload --clobber` (idempotent on re-runs).
Publishes stay fully isolated — cargo/PyPI/npm all run upstream of github-release,
so a Sigstore hiccup here can never block or corrupt a publish; at worst the
release just lacks the provenance asset. Signed-Releases climbs over the next
releases as each tag carries the bundle.
The v0.4.0-era CI failure was a runner network blip — `napi build` invokes cargo,
whose fetch of index.crates.io hit "Could not resolve host: index.crates.io" and
failed the Node-on-macOS job, forcing a manual re-run. The earlier flake-hardening
(setup-node/setup-python + rust-cache retries) only covered toolchain download and
cache restore, not the registry fetches inside the actual build/publish steps.
Set tool-level network retries as workflow env so every cargo/napi/maturin/
wasm-pack/npm/pip invocation in every job inherits them — including the nested
cargo calls inside napi/maturin/wasm-pack:
- CARGO_NET_RETRY=10 (default 3): cargo classes DNS-resolve / connect / timeout
errors as spurious and retries with backoff; 10 attempts ride out a transient
blip instead of failing the job.
- CARGO_NET_GIT_FETCH_WITH_CLI=true: more robust git-dep fetches.
- npm_config_fetch_retries=5 / maxtimeout=120s: npm ci/install registry retries.
- PIP_RETRIES=5 / PIP_DEFAULT_TIMEOUT=120: pip install resilience.
Applied to ci.yml, release.yml and bench.yml (the workflows that build). No more
manual re-runs for transient registry flakes.
* ci(sync-about): fix docs version-sync clone collision + webpage npm race
Two real release-time bugs surfaced by the v0.4.0 release, where the docs
"Published versions" table never updated and the marketing-site Cloudflare
build failed:
1. docs version sync never ran. The "Sync docs version (wickra-docs)" step
cloned into a directory literally named `docs`, but on a tag push the job
checks out the wickra repo at the workspace root, which already contains a
top-level `docs/` directory. `git clone … docs` therefore failed with
"destination path 'docs' already exists", silenced by `2>/dev/null` and
misreported as a missing-token warning, so the docs version table stayed at
the previous release. Clone into `docs-ver` instead (mirrors the `docs-count`
dir the count step already uses); it collides with nothing in the repo.
2. webpage build broke on a version race. The "Sync webpage version" step bumps
package.json's `wickra-wasm` pin to the released version and pushes
immediately, but release.yml publishes wickra-wasm to npm in parallel on the
same tag and finishes minutes later. Cloudflare's `npm clean-install` then
hit `ETARGET: No matching version found for wickra-wasm@^0.4.0`. Poll npm for
wickra-wasm@<version> (up to ~15 min) before committing; if it never appears
the step skips with a warning rather than pushing a build-breaking commit.
Both steps were designed to mirror each other across docs/webpage; these fixes
restore that symmetry so every release self-heals both sites.
* ci(sync-about): regenerate webpage package-lock on version bump
Third v0.4.0 release-sync defect: the webpage version step seds package.json's
wickra-wasm pin but never touched package-lock.json, so even after wickra-wasm
went live on npm the Cloudflare build still failed with
`npm ci` EUSAGE: "lock file's wickra-wasm@0.3.1 does not satisfy
wickra-wasm@0.4.0".
After the package.json sed, run `npm install --package-lock-only` so the lockfile
(version + resolved + integrity) matches the new pin; commit package-lock.json
alongside package.json. The earlier npm-wait already guarantees the version is
resolvable. Guarded: if the regen fails the step skips the whole commit rather
than push a package.json/lock mismatch.
The live site was unblocked out-of-band by a matching lockfile commit on the
webpage repo; this makes it self-heal on every future release.
The auto-injected GITHUB_TOKEN defaulted to write-all in ci.yml, bench.yml
and release.yml (no top-level permissions block), and codeql.yml declared
its scopes only at job level. Add a top-level `permissions: contents: read`
to all four so the token starts read-only and only the jobs that genuinely
write through it raise the scope:
- release.yml: github-release keeps contents: write; node-/wasm-publish and
attestations keep their id-token / attestations: write blocks. The
cargo/python/node publish jobs push to crates.io/PyPI/npm via their own
registry secrets, not the GITHUB_TOKEN, so read-only is correct for them.
- codeql.yml: analyze keeps security-events: write (job level).
- ci.yml / bench.yml: no job writes back to the repo (coverage uploads via
CODECOV_TOKEN; bench only uploads an artifact), so no job override is needed.
sync-about.yml already had a top-level block but at contents: write; demote
the top level to read and move contents: write down to the single `sync` job
(the PR-head counter push is the only GITHUB_TOKEN write). The cross-repo
About/docs/webpage/org writes are unaffected — they run through the
fine-grained ABOUT_SYNC_TOKEN, which the permissions key does not govern.
Raises OpenSSF Scorecard Token-Permissions from 0 toward 10.
Minor release. The headline user-facing change is the Node binding now
rejecting invalid indicator periods instead of silently clamping period 0
to 1 (matches Python/WASM/core); plus per-ecosystem binding READMEs and a
corrected MSRV statement in CONTRIBUTING. See CHANGELOG [0.4.0].
- Cargo.toml (workspace.package + wickra-core dep) + Cargo.lock (6 members)
- bindings/python/pyproject.toml
- bindings/node/package.json (version + 6 optionalDependencies) + package-lock.json
- bindings/node/npm/*/package.json (6 platform subpackages)
- examples/node/package-lock.json (wickra-* platform pins)
- CHANGELOG: [Unreleased] -> [0.4.0] - 2026-05-31 + compare URL
No tag pushed (release publish is a separate, user-confirmed step).
Embed the brand banner at the top of the README (linked to wickra.org) and
drop the now-redundant "# Wickra" heading — the banner shows the wordmark.
The image is served from https://wickra.org/og-banner.webp, which the webpage
build regenerates on every deploy from the current indicator count (4K WebP),
so the README banner stays current with no committed binary in this repo and
renders on GitHub, crates.io and docs.rs alike.
Now that the wickra.org catch-all mailbox exists, move the project contact +
package-author email off the personal gmail to support@wickra.org across all
surfaces: CODE_OF_CONDUCT, SECURITY, CITATION.cff, Cargo.toml, the npm + PyPI
author fields, the release.yml npm author, and repo-metadata.toml. (The
package-author changes take effect on the next published release.)
repo-metadata.toml's [audit].forbidden still pins kingchencp@gmail.com (the
private commit email) as a banned substring — unchanged.
Also remove the FUNDING.yml custom "https://wickra.org/sponsor" entry: that
page 404s, so the Sponsor button linked to a dead URL. The GitHub Sponsors
entry (github: [kingchenc]) stays.
* ci(sync-about): auto-sync the published version to wickra-docs on release
* ci(sync-about): retarget indicator-count sync from wiki to wickra-docs + point About homepage at docs.wickra.org (P8.3)
* ci(sync-about): self-update the marketing site (webpage) count + version (P12.1)
* ci(sync-about): auto-sync the published version to wickra-docs on release
* ci(sync-about): retarget indicator-count sync from wiki to wickra-docs + point About homepage at docs.wickra.org (P8.3)
The documentation now lives in the wickra-lib/wickra-docs VitePress repo and
will deploy to docs.wickra.org. Rewrite every tracked, user-facing wiki link
in the main repo to the new canonical site:
- docs/README.md, CONTRIBUTING.md, PULL_REQUEST_TEMPLATE.md
- bindings/{node,python,wasm}/README.md, examples/wasm/README.md
- repo-metadata.toml: wiki_url/wiki_git -> docs_url/docs_git
Page paths map 1:1 to VitePress clean URLs (e.g. /wiki/Quickstart-Rust.md ->
docs.wickra.org/Quickstart-Rust). The sync-about.yml wiki-sync step is left
untouched on purpose: it stays until the docs site is live (tracked as P8.3).
The GitHub wiki itself is not deleted yet for the same reason.
* build(node): track the generated index.d.ts (P5.1)
index.js was committed but index.d.ts was gitignored, an inconsistency that
also contradicts CONTRIBUTING ('regenerate both .d.ts/.js when a binding API
changes'). Track index.d.ts too so the repo carries the TypeScript types as a
matched pair with index.js. Generated by napi build; ~214 indicator classes.
* fix(node): reject invalid periods instead of clamping them (P5.4)
The Node scalar-indicator macro clamped period 0 to 1 (via clamp_period + must)
and the multi-parameter constructors did the same, silently swallowing the
core's PeriodZero validation. The core rejects period 0 (Error::PeriodZero),
and the Python and WASM bindings already propagate it — Node was the outlier,
masking caller mistakes. Make the macro constructor fallible and let every
constructor propagate the core error via map_err, removing clamp_period/must.
Update the smoke test (period 0 now throws, matching core/Python/WASM).
* docs(changelog): note the Node period-validation change (P5.4)
Behavior change per CONTRIBUTING: Node constructors now reject invalid periods
instead of clamping. Add an [Unreleased] Changed entry.
* chore: drop accidentally committed scratch log
The .gitignore comment claimed package-lock.json is committed only under
bindings/node/, but examples/node/package-lock.json has also been tracked
since #80. Correct the comment and add a CONTRIBUTING 'Lockfile policy'
section spelling out every component: Cargo.lock + the two Node package-locks
are tracked; fuzz/Cargo.lock is ignored (cargo-fuzz default); the Python
package has no lockfile by PyO3 convention (pinned via Cargo.lock); the
ghost-ignored site keeps its lockfile local.
* examples(node): add RSI mean-reversion strategy
Node counterpart of strategy_rsi_mean_reversion.{py,rs}: RSI(14) < 30 long,
> 70 exit, 0.1% fees, hourly BTCUSDT. Output verified byte-identical to the
Rust reference (37 trades W24/L13, -17.84% return, 46.89% max drawdown).
* examples(node): add MACD + ADX trend-filter strategy
Node counterpart of strategy_macd_adx.{py,rs}: MACD(12,26,9) histogram
crossover entries gated by ADX(14) > 20, hourly BTCUSDT, 0.1% fees. Output
verified byte-identical to the Rust reference (246 trades W90/L156, -47.19%
return, 53.75% max drawdown).
* examples(node): add Bollinger-squeeze breakout strategy
Node counterpart of strategy_bollinger_squeeze.{py,rs}: enter on a fresh
180-bar Bollinger-bandwidth low + close above the upper band, exit on a
2*ATR(14) stop or upper-band collapse, daily BTCUSDT, 0.1% fees. Output
verified byte-identical to the Rust reference (1 trade, -7.82% return,
13.01% max drawdown).
* examples(wasm): add RSI mean-reversion strategy demo
Browser counterpart of strategy_rsi_mean_reversion.{py,js,rs}: RSI(14) < 30
long, > 70 exit, 0.1% fees, summary table. Same signal/fill/PnL/equity loop as
the runtime-verified Node example; loads via the established wickra_wasm.js
init + fetch-CSV pattern. (wasm32 build runs in CI.)
* examples(wasm): add MACD + ADX trend-filter strategy demo
Browser counterpart of strategy_macd_adx.{py,js,rs}: MACD(12,26,9) histogram
crossover gated by ADX(14) > 20, hourly BTCUSDT, 0.1% fees. Logic identical to
the runtime-verified Node example; standard wickra_wasm.js init + fetch-CSV
loader. (wasm32 build runs in CI.)
* examples(wasm): add Bollinger-squeeze breakout strategy demo
Browser counterpart of strategy_bollinger_squeeze.{py,js,rs}: fresh 180-bar
Bollinger-bandwidth low + upper-band breakout, 2*ATR(14) stop, daily BTCUSDT,
0.1% fees. Logic identical to the runtime-verified Node example; standard
wickra_wasm.js init + fetch-CSV loader. (wasm32 build runs in CI.)
* ci: add examples syntax-smoke job (P2.3)
The Rust examples are built by 'cargo build -p wickra-examples --bins'; the
Node, browser-WASM and Python examples had no build gate. New job parse-checks
every examples/{node,wasm}/*.js, extracts and node --checks each WASM .html
module script, and python -m py_compile's every examples/python/*.py — so a
broken example edit fails CI instead of landing silently.
* docs(examples): list the new Node + WASM strategy examples
Add the three Node strategy scripts and three WASM strategy demos to the
examples README tables, bringing Node and WASM to parity with the existing
Rust and Python strategy rows.
* chore(examples): refresh examples/node lockfile for the linked wickra binding
npm install rewrote the file: dependency snapshot of the local wickra binding
that the examples link against (version 0.1.4 -> 0.3.1, license + engines
fields), which had gone stale in the committed lockfile.
* test(node): add input-validation suite
Node counterpart of bindings/python/tests/test_input_validation.py: invalid
constructor parameters (ATR zero period, MACD non-increasing fast/slow,
BollingerBands negative multiplier, PSAR step > max, ValueArea period/pct,
InitialBalance/OpeningRange zero period, Ichimoku non-increasing periods,
Ehlers-family ordering) and unequal-length candle/ValueArea batch inputs all
throw a JS Error. Validated against the built binding.
* test(node): add indicator completeness contract
Introspects every exported indicator class and asserts the full interface
(update / batch / reset / isReady / warmupPeriod) plus the pre-warmup contract
for zero-arg indicators, and guards that the full catalogue (>= 200 classes)
is exported. Catches a new indicator wired without the standard methods, or a
stale/partial native build dropping exports, with no per-indicator boilerplate.
* test(wasm): broaden scalar streaming-vs-batch coverage
Extend the inline wasm-bindgen-test suite with a streaming==batch check across
~70 scalar indicators spanning moving averages, momentum, volatility,
statistics/regression, Ehlers/cycle and risk/performance families (previously
only EMA + the candle-input group were covered per-indicator), plus four more
invalid-constructor assertions. Constructor args mirror the CI-passing Node
factories. Host-compiles (cargo test -p wickra-wasm --no-run); executed in CI
via wasm-pack test --node.
* bench(node): add indicator throughput benchmark
Node counterpart of the Rust criterion benches / Python compare_libraries:
measures streaming (per-tick update) and batch throughput in Mupd/s across a
representative indicator set over a synthetic OHLCV series (--bars, default
200k). Dependency-free; wired as 'npm run bench'.
* docs(wasm): list strategy demos + document the benchmark story
Add the three new strategy demos to the WASM examples table and a Performance
section: parallel_assets.html is the in-browser benchmark, with raw throughput
covered by the Rust criterion / Python / Node benchmarks (the WASM engine is the
same core compiled to wasm32).
Extend the count-sync workflow to drive three more surfaces from the same
single count, so the org page never drifts again:
- Repo About **homepage** URL — enforced unconditionally via
`gh repo edit --homepage` in the existing About step (fixes the stale
kingchenc URL and self-heals). Uses the same Administration-write
permission as --description, so NO extra token scope.
- Org **profile README** count (wickra-lib/.github, profile/README.md) —
clone + sed + bot commit/push, same pattern as the Sync Wiki step.
- Org **description** field ('… N indicators, install-free.') — gh api PATCH.
The two org steps need ABOUT_SYNC_TOKEN scope the main-repo syncs do not
(write on wickra-lib/.github; admin:org for the description PATCH). Both are
written to soft-skip with a ::warning:: and continue-on-error, so the run
stays green until the scope is granted — then they sync with no further
change. Homepage swap-point to a docs domain is a one-line change.
Refs findings P9 / P10.
* chore: remove ROADMAP.md from the public repo
ROADMAP is kept as a local-only draft (ghost-ignored via
.git/info/exclude); it is not part of the published package surface.
* release: bump 0.3.0 -> 0.3.1
CI-only patch: fixes the release.yml CycloneDX SBOM step (cargo-cyclonedx
has no -p flag, see #79) that skipped the GitHub Release attach-assets job
on 0.3.0. No library changes — republishes the same code with a working
release pipeline.
- Cargo.toml (workspace.package + wickra-core dep) + Cargo.lock
- bindings/python/pyproject.toml
- bindings/node/package.json (version + 6 optionalDependencies) + package-lock.json
- bindings/node/npm/*/package.json (6 platform subpackages)
- CHANGELOG: finalize [0.3.0] (was still under [Unreleased]), add [0.3.1]
* chore: track examples/node/package-lock.json
Since the global package-lock ignore rule was dropped (#68) this file was
left untracked. Commit it for reproducible example installs, consistent
with bindings/node (findings P4.1).
cargo-cyclonedx 0.5.9 walks the whole workspace in a single pass and
writes a <package>.cdx.json next to each member's Cargo.toml; it has no
-p/--package selector. The previous three 'cargo cyclonedx ... -p <crate>'
invocations aborted with 'error: unexpected argument -p found', failing
the cargo-publish job after the crates were already published and, in
turn, skipping the github-release attach-assets job (it needs all four
publish jobs). v0.3.0 published to every registry but got no GitHub
Release page or SBOM assets as a result.
Replace the three invalid calls with a single workspace pass and copy
the three crates.io crate SBOMs into the upload dir.
Minor bump (not patch) because the [Unreleased] section since 0.2.7 has
accumulated a sweep of additive changes that justify a new minor:
- Family 9-16 indicator catalogue expansion (Bands & Channels, Trailing
Stops, Volume, Statistics, Ehlers/Cycle DSP, Pivots, DeMark, Ichimoku,
Candlestick Patterns, Market Profile, Risk/Performance) — roughly
100+ new indicators since 0.2.7 across all four bindings.
- New `wickra_core::FAMILIES` const + family-taxonomy guard tests.
- GitHub org migration (kingchenc -> wickra-lib) and new maintainer
email (wickra.lib@gmail.com).
- New `repo-metadata.toml` + `sync-metadata.yml` audit workflow.
- WASM CI tests now run on every PR (existing tests had been
manually-only).
- CycloneDX SBOMs + npm provenance attestations attached to releases.
- Three end-to-end strategy examples.
- Governance polish: ARCHITECTURE / ROADMAP / CITATION / FUNDING /
.editorconfig.
- Curated benchmark suite (~33 representative indicators).
- Three cold-path coverage fixes (mama, rsi, sine_wave).
- bindings/node/package-lock.json now committed.
Workspace + bindings (Rust crate, Python wheel, Node main + 6 platform
sub-packages, WASM) all step to 0.3.0. CHANGELOG opens the [0.3.0]
section dated 2026-05-28 with the full Changed / Added inventory.
Compare-URL block adds the v0.2.7...v0.3.0 line under [Unreleased] and
points [Unreleased] at v0.3.0...HEAD using the new wickra-lib org.
**Supersedes PR #61** (0.2.7 -> 0.2.8 patch bump). Close#61 when this
one merges. Merge ordering remains: #59 (org migration) + #60
(family-api) + the polish PRs first, rebase this PR on top of the new
main, then merge.
Tag-push `v0.3.0` is a SEPARATE, manual step after merge — it triggers
release.yml's irreversible publish to crates.io / PyPI / npm.
A full `cargo doc --workspace` (and docs.rs, which builds with
`-D rustdoc::all`) emitted five broken intra-doc links. docs.rs treats
these as hard errors, so any 0.2.x doc build was at risk of aborting.
- mama.rs: `[`Fama`]` -> `[`crate::Fama`]` (Fama lives in another module)
- standard_error.rs: `[`crate::Bollinger`]` -> `[`crate::BollingerBands`]`
(the public type is `BollingerBands`, not `Bollinger`)
- aggregator.rs: drop the link to the private `OpenBar::into_candle`,
keep it as plain code text
- resample.rs: drop the link to the private `RolledBar::into_candle`
- csv.rs: `CandleReader::with_timestamp_parser` never existed; reword to
state plainly that ISO/RFC timestamps must be converted to integers
Verified with `RUSTDOCFLAGS="-D rustdoc::broken_intra_doc_links \
-D rustdoc::private_intra_doc_links" cargo doc --workspace --no-deps`:
clean, zero warnings.
Until now `package-lock.json` was globally ignored. Two practical
consequences for the Node binding:
- A fresh `git clone && cd bindings/node && npm install` resolved
`@napi-rs/cli` and any transitive deps to whatever the npm registry
currently considered the latest matching the package.json semver
ranges. Contributors could get different dep graphs on different days.
- No protection against transitive-dep tampering at install time
(lockfile records resolved versions + integrity hashes, npm verifies
on subsequent installs).
Drop the global `package-lock.json` ignore and commit the freshly
generated `bindings/node/package-lock.json` (140 lines, only a couple
of direct deps because the binding is small). The `.gitignore` comment
notes that we still don't expect lockfiles at the workspace root.
No CI workflow changes — the existing `npm install` in the Node-test job
will now consume the committed lockfile, which is exactly what we want.
Two modern supply-chain-trust additions to the release pipeline,
neither of which changes what gets published — only adds verifiable
signals attached to existing releases.
1. **CycloneDX SBOMs.** `cargo-cyclonedx` is installed in the
cargo-publish job after the .crate files are built, and runs once
per published crate (`wickra-core`, `wickra-data`, `wickra`). The
resulting `*.cdx.json` files are uploaded as the `sboms` artifact,
then attached to the GitHub Release alongside the existing wheels,
tarballs, .node binaries and .crate files. Future security advisories
can answer "is my version of crate X transitive in wickra Y.Z?" by
reading the SBOM directly instead of resolving the lockfile.
2. **npm `--provenance` flag** on every npm publish call:
- main `wickra` package (node-publish, first + retry)
- per-platform `wickra-<triple>` subpackages (node-publish loop)
- `wickra-wasm` (wasm-publish)
Provenance attestations are generated server-side by npm from the
GitHub Actions OIDC token. The publishing jobs gain
`permissions: id-token: write` so the runner can exchange that
token. The npm page for each published version will then carry the
"Verified provenance" badge, which proves the tarball was built by
*this* workflow run and not by an arbitrary local laptop with the
NPM_TOKEN.
Skipped deliberately (to keep this PR focused, possibly follow-ups):
- Sigstore cosign signing of artefacts (different audit story; can be
layered on after npm-provenance lands).
- SLSA build-provenance attestations via `actions/attest-build-provenance`
(would target every artefact uniformly; the npm-provenance flag is
the more pragmatic first cut).
YAML structure validated (8 jobs intact). No production code touched.
This PR conflicts with PR #59 only in line-by-line URL substitutions
on release.yml — rebase after #59 should be clean.
Wires real indicators into complete signal -> fill -> PnL -> equity
loops over the checked-in BTCUSDT datasets, with per-trade Sharpe and
max-drawdown reported on stdout. Closes the gap where existing
examples showed only the mechanics of calling `update`/`batch` but not
how Wickra plugs into a trading-system shape.
Three strategies, each in Rust + Python (six files total):
- strategy_rsi_mean_reversion — RSI(14) thresholds (30/70) on 1h
BTCUSDT. Binary position, 0.1% per-trade fee.
- strategy_macd_adx — MACD crossover entries gated by ADX(14) > 20 on
1h BTCUSDT. Trend-follower demo of multi-indicator gating.
- strategy_bollinger_squeeze — Bollinger-bandwidth 180-day-low squeeze
+ upper-band breakout entry, ATR(14) * 2 stop. On 1d BTCUSDT for
interpretable lookback.
Each file is self-contained — print_summary is inlined per script so
the example stays a single-file read. Every script prints a
NOT-financial-advice notice next to its results.
examples/README.md updated to list the new bins/scripts.