Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 654da5722f | |||
| aacb9280f1 | |||
| d2bc000892 | |||
| 1f4bf9e3a6 | |||
| d36d514f56 |
@@ -5,5 +5,7 @@
|
||||
maturin
|
||||
numpy
|
||||
pandas
|
||||
TA-Lib
|
||||
tulipy
|
||||
talipp
|
||||
finta
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# ./scripts/update-lockfiles.sh
|
||||
build==1.5.0 \
|
||||
--hash=sha256:13f3eecb844759ab66efec90ca17639bbf14dc06cb2fdf37a9010322d9c50a6f \
|
||||
--hash=sha256:302c22c3ba2a0fd5f3911918651341ebb3896176cbdec15bd421f80b1afc7647
|
||||
# via ta-lib
|
||||
colorama==0.4.6 \
|
||||
--hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \
|
||||
--hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6
|
||||
# via build
|
||||
finta==1.3 \
|
||||
--hash=sha256:b94b94df311c18bf5402eb2fe8fd2db5e1bdaff08baf58a7367d05c7abdd10d3 \
|
||||
--hash=sha256:f2fa0673748f4be8f57e57cf6d5c00a4d44bc6071ea69dbb9a1d329d045cbba2
|
||||
@@ -97,6 +105,12 @@ numpy==2.4.6 \
|
||||
# -r .github/requirements/bench.in
|
||||
# finta
|
||||
# pandas
|
||||
# ta-lib
|
||||
# tulipy
|
||||
packaging==26.2 \
|
||||
--hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e \
|
||||
--hash=sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661
|
||||
# via build
|
||||
pandas==3.0.3 \
|
||||
--hash=sha256:0383c72c75cdcca61a9e116e611143902dbfd08bff356829c2f6d1cf40a9ca8c \
|
||||
--hash=sha256:05f1f1752b8533ea03f7f39a9c15b1a058d067bb48f4748948e7a8691e0510f2 \
|
||||
@@ -149,6 +163,10 @@ pandas==3.0.3 \
|
||||
# via
|
||||
# -r .github/requirements/bench.in
|
||||
# finta
|
||||
pyproject-hooks==1.2.0 \
|
||||
--hash=sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8 \
|
||||
--hash=sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913
|
||||
# via build
|
||||
python-dateutil==2.9.0.post0 \
|
||||
--hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \
|
||||
--hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427
|
||||
@@ -157,10 +175,72 @@ six==1.17.0 \
|
||||
--hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \
|
||||
--hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81
|
||||
# via python-dateutil
|
||||
ta-lib==0.6.8 \
|
||||
--hash=sha256:02388054c059945e5f02625f5075bac20a1803573cb43e7d096091027511961f \
|
||||
--hash=sha256:094677b279a59c3f01c3aca8a889fda3523fd641a3805f69a2d642121b72e55e \
|
||||
--hash=sha256:0a08a29690a922ba92a6cf42902a8a93c6fbda4cfed62c3c5b0471560ef60135 \
|
||||
--hash=sha256:0ccd478ff5735831bf2a61d653466bfda8afadc26ad58ca6b1edb9e7521cc674 \
|
||||
--hash=sha256:0e371d14b49e70caa973a234c8823341dd446f5c5d7acc826868bb42b272bdc0 \
|
||||
--hash=sha256:11a373c9308eae3bac2d56d37017f9ab63968cc074a8b95be879aae3d13133aa \
|
||||
--hash=sha256:128ec92e6a0e9ff7a38edef80e3b74f15bb2ed1c531d5d3252c8dca22677651b \
|
||||
--hash=sha256:1fb4028437201e19014e4e374272b739867c8a3eb655da46675ef4c2ff14b616 \
|
||||
--hash=sha256:282e49c766b5952dd8796f77d7ed3ae412cdd88e31f845b1fbbb86ac6cb7bebf \
|
||||
--hash=sha256:2b369cabb48485fbf444beb3f5a878075367b99c2c86db2f796afeabebc749e0 \
|
||||
--hash=sha256:2bf714333788bf5175f2512b86d2ed129e89ae6f6c2923e8a297a1e3395e13b5 \
|
||||
--hash=sha256:30de46b55873b51be945a09edf486afcc190dc47eff9fb5d2b12c9f7e3d743da \
|
||||
--hash=sha256:34e3b12407ddf99f6627435aa8a165f094339bb7dc33de92e1d7472e9f237304 \
|
||||
--hash=sha256:36b2a516fce57309840f5ef3fa2fd0c4449293fc72536a0400d2e1e26b414da8 \
|
||||
--hash=sha256:3a9195299df9d7d2a6e9d16bebd6b706b0ea99e4b871864c4b034c2577e21a77 \
|
||||
--hash=sha256:3c32fc0f546ceecc47dd45f33d72ab4a1e341b80d9081c2d77b100add5d49104 \
|
||||
--hash=sha256:3d7333e907bff3e3997e54f89733ffa8d619842a3e1cd962bca34bdc11944c28 \
|
||||
--hash=sha256:4795e93d130c9b7fb661f0cead49752ae6a980437df74b99d5918026c212443e \
|
||||
--hash=sha256:490e19a45cd3cdd6dfe6b46019f7ffe1103500750b41b51996a870e7c1c5f066 \
|
||||
--hash=sha256:4aa0fe08383f3e5fc7d2f8cf9b42ac778f4d53fd75bcd2799a858225954eab89 \
|
||||
--hash=sha256:559326d8f3d904cd4aa61f6a392d5626f35eec6a9f6cc83bcddb0abf88c40516 \
|
||||
--hash=sha256:5929c83bd8cb7572d1c17ffdbf0eac235bf3c4d53cde1950cf89d944eaf97525 \
|
||||
--hash=sha256:5bfd21b6acb32e20d4e279c34405a34e63da345be4b2b6eabd683e1a88857406 \
|
||||
--hash=sha256:613cf06313331f49dd7b85a5a24fbddb1156c9723b6921a231906241726e5aee \
|
||||
--hash=sha256:66a8e1c1e899d15a2f7510e43527fba22d895e7f6058d027db3e3837d88a69de \
|
||||
--hash=sha256:691a62926ba09f2653ec0908554b3635497efb7751c5d46b916cd1ebbb1d3c25 \
|
||||
--hash=sha256:6c1fd18e45c39d5a4be4b0d6a20c141e43fe46daeb1b2e2f304ebae7015ab6e6 \
|
||||
--hash=sha256:6c6a1e8f98de92e817491b50aa4d01d69a1b41a4ed3173747e8f16f0d4cf81cc \
|
||||
--hash=sha256:6cf029b886cfb28a2701503b7c602b811f2daa45276bd6459b0c71e051deb497 \
|
||||
--hash=sha256:71506116eac0d3e3598d6325b4b818c3a0f6acb3222b24d30ad726e8c4bf7ea8 \
|
||||
--hash=sha256:7993164e8e9f78ec31d38c47850ca6ba5451788b5b49a8a2dbb3322b36b5693b \
|
||||
--hash=sha256:7a5cc6bf60791d8274edfdfe2dd7cec3f00f656dcc92e2b0a9af06c8b18ce6a6 \
|
||||
--hash=sha256:87c1cc1057d903b78a8257a7c5f497db6fd5284f5080392bd57b66031d7389a3 \
|
||||
--hash=sha256:98376c75bd6c103c74396953084a5e0798ffe476aecbfcc51ec6d100a685ac38 \
|
||||
--hash=sha256:a395524b0fafa10446d11e11acb4742e919523de58aac03b791f26d7a783bcf0 \
|
||||
--hash=sha256:a5100a4be91b7d4b7c8fe16a3600bd0951e10205eb1066b6873afd3996b51ee4 \
|
||||
--hash=sha256:a63a52221f8c73f82f4e00493351d987f594931198589287aee96f8da673cfd5 \
|
||||
--hash=sha256:a89734a7bcb2ea3b6fd600a74d6fbcdb8d3fa3f7917dbd978e039710b5509c9c \
|
||||
--hash=sha256:b165f5e6de1ccc964e863bd2035807a4d3bad3e0481f9db2dc52034d6ad4f9de \
|
||||
--hash=sha256:b3845e4c2fa32963fb7f384ebbaa2761b0e6b96145239bf80e956d4aff4b071c \
|
||||
--hash=sha256:b3b017d9103e7a7372a146773be32b184ff7330bd708d40b1f56f06a686756ed \
|
||||
--hash=sha256:b6c6e4858d8c3f88e19b7aa94b6a7619108f0bee51da9fa67b0785a8b59955f9 \
|
||||
--hash=sha256:bfad1202fb1f9140e3810cc607058395f59032d9128cc0d716900c78bea5f337 \
|
||||
--hash=sha256:c01809fb602e2fefc8cbfb3b603bb59d2a2eaee8708410896d48a835ba00e7c5 \
|
||||
--hash=sha256:cce8de9d48289927ed18aaa420740efd52b2cd9289da32e3799afbb3a02822e8 \
|
||||
--hash=sha256:ce2bc1ea01200b6d8130ab917296d05d77a1a571ec6c1ee25cfca6d55cd5db4a \
|
||||
--hash=sha256:d4601e2a8b46ffbf540601a4926fd6cc5aae8a13b36fdd467f1040f01f9edaed \
|
||||
--hash=sha256:d556d1c256b3700b60b6b061664a667b2e49d599c2772d46a9f2348f2dc4ab5c \
|
||||
--hash=sha256:ddf7453acd03b966624ebefdb38169b5bbbeea1a1a58c90b095667247f9de327 \
|
||||
--hash=sha256:e781eeb65b2007af553389c8a7fb7bc53cb856118b0fcffb2c26b0f49561c686 \
|
||||
--hash=sha256:e920c272cd9e70a6b10eae9203cc96845da142e1dd4482de9343dda3738a9862 \
|
||||
--hash=sha256:f5b6174bf4bf9152e368561dff410203c6921e4dd2afbcda3283a95957158112 \
|
||||
--hash=sha256:f69bd42fd2515060af69b120668213121264bb7976b113954b6f9db327727c65 \
|
||||
--hash=sha256:f823d0f6b04a6797fbe253bcf91666e71a6b63c290683819650c68b2468ebe64 \
|
||||
--hash=sha256:fa7e9f2e80a9535f9692e113d02b4268b5f88675a730d1b0ef0abeb74c9a4e80
|
||||
# via -r .github/requirements/bench.in
|
||||
talipp==2.7.0 \
|
||||
--hash=sha256:567f59ad74366cb59a14a00d350f35fd9d22e6924d6228bad581e6dcf1de2205 \
|
||||
--hash=sha256:f749f22b9ad615605e71faf26457bb7f5e3fe16f04d3287f4ca54fd16bc3d4eb
|
||||
# via -r .github/requirements/bench.in
|
||||
tulipy==0.4.0 \
|
||||
--hash=sha256:540704956b5b940a5f6306aa393a37536a6d7c3cbc07efe47512f3496e5203ab \
|
||||
--hash=sha256:95542e40537afdd345d875baf37485eac993c6a819d00c51432e9de8df21eba8 \
|
||||
--hash=sha256:fbc31727ef7657c93ad910bfdce65fecc6aaa7a5e961fe00240718e7a3fc79d8
|
||||
# via -r .github/requirements/bench.in
|
||||
tzdata==2026.2 \
|
||||
--hash=sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10 \
|
||||
--hash=sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7
|
||||
|
||||
@@ -117,3 +117,29 @@ jobs:
|
||||
with:
|
||||
name: cross-library-bench
|
||||
path: bindings/python/benchmark.txt
|
||||
|
||||
rust-cross-bench:
|
||||
name: Rust cross-library benchmark report
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable branch, 2026-03-27
|
||||
|
||||
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
|
||||
continue-on-error: true # cache is an optimisation; never block on a stuck/slow restore
|
||||
timeout-minutes: 6
|
||||
|
||||
# Wickra vs the other Rust TA crates (kand, ta-rs, yata) on an identical
|
||||
# candle series — the like-for-like engine comparison with no binding
|
||||
# overhead. Streaming + batch, in crates/wickra-bench/benches/cross_lib.rs.
|
||||
- name: Run Rust cross-library benchmark
|
||||
run: cargo bench -p wickra-bench --bench cross_lib | tee rust_cross_bench.txt
|
||||
|
||||
- name: Upload Rust report
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: rust-cross-bench
|
||||
path: rust_cross_bench.txt
|
||||
|
||||
+28
-1
@@ -7,6 +7,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.5.9] - 2026-06-06
|
||||
|
||||
### Added
|
||||
|
||||
- Internal Rust cross-library benchmark harness (`crates/wickra-bench`, not
|
||||
published) comparing Wickra against `kand`, `ta-rs` and `yata` on an identical
|
||||
candle series in both streaming and batch modes; wired into the nightly
|
||||
`cross-library-bench` workflow.
|
||||
- `tulipy` runners and expanded per-tick streaming coverage (SMA, EMA, RSI,
|
||||
MACD, Bollinger) in the Python `compare_libraries` benchmark.
|
||||
|
||||
### Changed
|
||||
|
||||
- Faster streaming and batch updates for SMA, Bollinger Bands, RSI, EMA and ATR
|
||||
(flat ring buffers replacing `VecDeque`, hoisted reciprocals in the Wilder
|
||||
smoothing, leaner hot state) — indicator outputs are unchanged.
|
||||
- Rewrote the README benchmark section into honest, tiered tables (Rust core vs
|
||||
the other Rust crates, and Python vs the Python ecosystem) that show where
|
||||
Wickra wins and where it loses, not only the favourable comparisons.
|
||||
|
||||
## [0.5.8] - 2026-06-04
|
||||
- **TSF Oscillator** — the percentage gap of the close to the one-bar-ahead time-series forecast, a close-relative companion to CFO (`TsfOscillator`).
|
||||
- **MACD Histogram** — the standalone macd-minus-signal bar of MACD as a scalar series (`MacdHistogram`).
|
||||
- **PPO Histogram** — the Percentage Price Oscillator with its signal EMA and the resulting zero-centered histogram (`PpoHistogram`).
|
||||
|
||||
## [0.5.7] - 2026-06-04
|
||||
- **Qstick** — Qstick (Chande), the SMA of the candle body (close − open) as a net buying/selling pressure gauge (`QSTICK`).
|
||||
- **TTM Trend** — TTM Trend (John Carter), +1/−1 by whether the close sits above the SMA of recent median prices (`TTM_TREND`).
|
||||
@@ -1268,7 +1293,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
optional Binance live feed.
|
||||
- Bindings for Python, Node.js, and WebAssembly.
|
||||
|
||||
[Unreleased]: https://github.com/wickra-lib/wickra/compare/v0.5.7...HEAD
|
||||
[Unreleased]: https://github.com/wickra-lib/wickra/compare/v0.5.9...HEAD
|
||||
[0.5.9]: https://github.com/wickra-lib/wickra/compare/v0.5.8...v0.5.9
|
||||
[0.5.8]: https://github.com/wickra-lib/wickra/compare/v0.5.7...v0.5.8
|
||||
[0.5.7]: https://github.com/wickra-lib/wickra/compare/v0.5.6...v0.5.7
|
||||
[0.5.6]: https://github.com/wickra-lib/wickra/compare/v0.5.5...v0.5.6
|
||||
[0.5.5]: https://github.com/wickra-lib/wickra/compare/v0.5.4...v0.5.5
|
||||
|
||||
Generated
+114
-7
@@ -702,6 +702,16 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kand"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af1f41590bd014ef6c3dd815b45f07deb4c3198e355a4319bb7521b6a3a6aeb5"
|
||||
dependencies = [
|
||||
"num_enum",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "leb128fmt"
|
||||
version = "0.1.0"
|
||||
@@ -911,6 +921,28 @@ dependencies = [
|
||||
"libm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num_enum"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26"
|
||||
dependencies = [
|
||||
"num_enum_derive",
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num_enum_derive"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8"
|
||||
dependencies = [
|
||||
"proc-macro-crate",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "numpy"
|
||||
version = "0.28.0"
|
||||
@@ -1081,6 +1113,15 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-crate"
|
||||
version = "3.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
|
||||
dependencies = [
|
||||
"toml_edit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
@@ -1498,6 +1539,12 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ta"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "609409d472a0a7d8d4dd9e19891bbdef546b9dce670c3057d0e02192dc541226"
|
||||
|
||||
[[package]]
|
||||
name = "target-lexicon"
|
||||
version = "0.13.5"
|
||||
@@ -1607,6 +1654,36 @@ dependencies = [
|
||||
"tungstenite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "1.1.1+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.25.12+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"toml_datetime",
|
||||
"toml_parser",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_parser"
|
||||
version = "1.1.2+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
|
||||
dependencies = [
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tungstenite"
|
||||
version = "0.29.0"
|
||||
@@ -1867,7 +1944,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra"
|
||||
version = "0.5.7"
|
||||
version = "0.5.9"
|
||||
dependencies = [
|
||||
"approx",
|
||||
"criterion",
|
||||
@@ -1876,9 +1953,21 @@ dependencies = [
|
||||
"wickra-data",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wickra-bench"
|
||||
version = "0.5.9"
|
||||
dependencies = [
|
||||
"criterion",
|
||||
"kand",
|
||||
"ta",
|
||||
"wickra",
|
||||
"wickra-data",
|
||||
"yata",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wickra-core"
|
||||
version = "0.5.7"
|
||||
version = "0.5.9"
|
||||
dependencies = [
|
||||
"approx",
|
||||
"proptest",
|
||||
@@ -1888,7 +1977,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-data"
|
||||
version = "0.5.7"
|
||||
version = "0.5.9"
|
||||
dependencies = [
|
||||
"approx",
|
||||
"csv",
|
||||
@@ -1905,7 +1994,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-examples"
|
||||
version = "0.0.0"
|
||||
version = "0.5.9"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
"tokio",
|
||||
@@ -1915,7 +2004,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-node"
|
||||
version = "0.5.7"
|
||||
version = "0.5.9"
|
||||
dependencies = [
|
||||
"napi",
|
||||
"napi-build",
|
||||
@@ -1925,7 +2014,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-python"
|
||||
version = "0.5.7"
|
||||
version = "0.5.9"
|
||||
dependencies = [
|
||||
"numpy",
|
||||
"pyo3",
|
||||
@@ -1934,7 +2023,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-wasm"
|
||||
version = "0.5.7"
|
||||
version = "0.5.9"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"js-sys",
|
||||
@@ -1991,6 +2080,15 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.51.0"
|
||||
@@ -2091,6 +2189,15 @@ version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
|
||||
|
||||
[[package]]
|
||||
name = "yata"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b4ef8ddfa3ccd93454262c0e60a43a2bbf403d404174e1815f7581d5028229f"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
version = "0.8.2"
|
||||
|
||||
+3
-2
@@ -8,11 +8,12 @@ members = [
|
||||
"bindings/wasm",
|
||||
"bindings/node",
|
||||
"examples/rust",
|
||||
"crates/wickra-bench",
|
||||
]
|
||||
exclude = ["fuzz"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.5.7"
|
||||
version = "0.5.9"
|
||||
authors = ["kingchenc <support@wickra.org>"]
|
||||
edition = "2021"
|
||||
rust-version = "1.86"
|
||||
@@ -24,7 +25,7 @@ keywords = ["finance", "trading", "indicators", "technical-analysis", "ta"]
|
||||
categories = ["finance", "mathematics", "science"]
|
||||
|
||||
[workspace.dependencies]
|
||||
wickra-core = { path = "crates/wickra-core", version = "0.5.7" }
|
||||
wickra-core = { path = "crates/wickra-core", version = "0.5.9" }
|
||||
|
||||
thiserror = "2"
|
||||
rayon = "1.10"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<p align="center">
|
||||
<a href="https://wickra.org"><img src="https://raw.githubusercontent.com/wickra-lib/.github/main/profile/wickra-banner.webp?v=420" alt="Wickra — streaming-first technical indicators" width="100%"></a>
|
||||
<a href="https://wickra.org"><img src="https://raw.githubusercontent.com/wickra-lib/.github/main/profile/wickra-banner.webp?v=423" alt="Wickra — streaming-first technical indicators" width="100%"></a>
|
||||
</p>
|
||||
|
||||
[](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml)
|
||||
@@ -48,7 +48,7 @@ Full documentation lives at **[docs.wickra.org](https://docs.wickra.org)**:
|
||||
[Node](https://docs.wickra.org/Quickstart-Node),
|
||||
[WASM](https://docs.wickra.org/Quickstart-WASM).
|
||||
- **Indicators** — a per-indicator deep dive (formula, parameters, warmup) for
|
||||
every one of the 420 indicators; start at the
|
||||
every one of the 423 indicators; start at the
|
||||
[indicators overview](https://docs.wickra.org/Indicators-Overview).
|
||||
- **Reference** — [warmup periods](https://docs.wickra.org/Warmup-Periods),
|
||||
[streaming vs batch](https://docs.wickra.org/Streaming-vs-Batch),
|
||||
@@ -60,83 +60,135 @@ Full documentation lives at **[docs.wickra.org](https://docs.wickra.org)**:
|
||||
|
||||
## Why Wickra exists
|
||||
|
||||
The Python TA ecosystem has plenty of libraries — TA-Lib, pandas-ta, finta,
|
||||
talipp, tulipy — and every one of them shares the same blind spot:
|
||||
Wickra started as a personal itch. The existing TA libraries never quite fit the
|
||||
projects I was building, so I decided to build one from the ground up — partly to
|
||||
learn, partly because I genuinely enjoy taking something that already exists and
|
||||
trying to do it differently (and, ideally, better). It's open source because the
|
||||
useful version of that itch is the one other people can build on too.
|
||||
|
||||
| Library | Install pain | Streaming | Multi-language | Active |
|
||||
|------------------------|-----------------|-----------|----------------|--------|
|
||||
| **★ Wickra** | **clean** | **yes** | **Python + Node + WASM + Rust** | **yes** |
|
||||
| TA-Lib (Python) | yes (C deps) | no | no | barely |
|
||||
| pandas-ta | clean | no | no | slow |
|
||||
| finta | clean | no | no | stale |
|
||||
| ta-lib-python | yes (C deps) | no | no | barely |
|
||||
| talipp | clean | yes | no | yes |
|
||||
| Tulip Indicators | yes (C deps) | no | partial | stale |
|
||||
| ooples (C#) | clean | no | C# only | yes |
|
||||
Plenty of TA libraries are fast. Each one forces a trade-off Wickra does not:
|
||||
|
||||
Wickra is the only library that combines all of: clean install, streaming,
|
||||
multi-language reach, and active maintenance.
|
||||
| Library | Install | Streaming | Languages | Indicators | Active |
|
||||
|------------------|-------------|-------------|-----------------------------|-----------:|--------|
|
||||
| **★ Wickra**| **clean** | **yes, O(1)** | **Python · Node · WASM · Rust** | **423** | **yes** |
|
||||
| kand | clean | yes | Python · WASM · Rust | ~60 | yes |
|
||||
| ta-rs | clean | yes | Rust only | ~30 | stale |
|
||||
| yata | clean | partial | Rust only | ~35 | yes |
|
||||
| TA-Lib | yes (C deps)| no | many bindings | ~150 | barely |
|
||||
| pandas-ta | clean | no | Python | ~130 | slow |
|
||||
| finta | clean | no | Python | ~80 | stale |
|
||||
| talipp | clean | yes | Python | ~40 | yes |
|
||||
|
||||
## Benchmark: how much faster is "streaming-first"?
|
||||
Wickra's edge is **breadth with reach**: 423 indicators that all update in O(1)
|
||||
per tick and ship natively to Python, Node.js, WebAssembly and Rust from a
|
||||
single engine.
|
||||
|
||||
The numbers below were measured on a single developer workstation and are not
|
||||
guaranteed to reproduce identically on different hardware — absolute µs values
|
||||
depend on CPU, memory clock and OS scheduler. Read them as **relative
|
||||
speedups** between libraries on identical input, not as a universal
|
||||
performance contract.
|
||||
**On speed — and why Wickra isn't the fastest.** It deliberately isn't. The
|
||||
leaner Rust crates (kand, ta-rs) win several of the micro-benchmarks below, and
|
||||
those losses are shown rather than hidden. The gap is a *choice*, not a ceiling:
|
||||
every `update` validates its input, runs a real warmup before it emits a value,
|
||||
and returns an `Option` so a single bad tick can't silently poison the state.
|
||||
ta-rs, by contrast, hands back a bare `f64` from the first tick with no
|
||||
validation. If Wickra threw all of that away — raw `f64` out, no checks, no
|
||||
warmup contract — it would match or beat the leanest crate on every row. It
|
||||
keeps the guarantees instead, and still wins RSI, Bollinger and ATR against kand.
|
||||
What no other library matches is the *combination*: catalogue size, native O(1)
|
||||
streaming, NaN-safety, and four first-class language targets at once.
|
||||
|
||||
## Benchmarks
|
||||
|
||||
Three comparisons, split by layer and mode. Read them as **relative** speedups
|
||||
on identical input — absolute µs depend on CPU, memory clock and OS scheduler,
|
||||
not a universal contract.
|
||||
|
||||
- **Reproduced on:** Windows 11 Pro 26200, AMD Ryzen 9 9950X, 64 GB DDR5,
|
||||
Rust 1.92 (release profile, `lto = "fat"`, `codegen-units = 1`),
|
||||
Python 3.12, Node 20.
|
||||
- **Reproduce yourself:** `pip install -e bindings/python[bench]` then
|
||||
`python -m benchmarks.compare_libraries`. The script auto-detects every
|
||||
installed peer library and runs them on the same generated inputs as
|
||||
Wickra. The CI job `cross-library-bench` runs the same script on every
|
||||
push and uploads the raw report as a build artefact.
|
||||
Rust 1.92 (release: `lto = "fat"`, `codegen-units = 1`), Python 3.12.
|
||||
- **Reproduce yourself:**
|
||||
- Rust core vs Rust crates: `cargo bench -p wickra-bench`
|
||||
- Python vs Python libs: `pip install -e bindings/python[bench]` then
|
||||
`python -m benchmarks.compare_libraries` (auto-detects installed peers).
|
||||
|
||||
Lower µs/op = faster. Wickra wins every batch category outright, and the
|
||||
streaming gap widens linearly with how much history a batch-only library has
|
||||
to recompute on every tick.
|
||||
### 1. Rust core vs the other Rust TA crates
|
||||
|
||||
### Batch — single full pass over a 20 000-bar series
|
||||
Like-for-like, no language-binding overhead, over a 50 000-bar series (µs for
|
||||
the whole series, lower = faster). This is the honest engine comparison —
|
||||
Wickra wins some and loses some, and both are shown.
|
||||
|
||||
Reading the table: each cell shows that library's runtime, plus how many times
|
||||
slower it is than Wickra in parentheses. **★** marks the winner per row.
|
||||
**Streaming** (one value fed per `update`):
|
||||
|
||||
| Indicator | **★ Wickra** | finta | talipp |
|
||||
|---------------------|---------------------|-----------------------------|-------------------------------|
|
||||
| SMA(20) | **95.6 µs ★** | 343.5 µs (3.6× slower) | 7 640.6 µs (79.9× slower) |
|
||||
| EMA(20) | **64.6 µs ★** | 223.1 µs (3.5× slower) | 12 160.9 µs (188.2× slower) |
|
||||
| RSI(14) | **126.2 µs ★** | 1 107.1 µs (8.8× slower) | 15 792.2 µs (125.1× slower) |
|
||||
| MACD(12, 26, 9) | **119.0 µs ★** | 531.8 µs (4.5× slower) | 49 788.1 µs (418.2× slower) |
|
||||
| Bollinger(20, 2.0) | **105.3 µs ★** | 812.0 µs (7.7× slower) | 130 938.3 µs (1 243.7× slower)|
|
||||
| ATR(14) | **123.5 µs ★** | 5 144.8 µs (41.7× slower) | 28 816.0 µs (233.4× slower) |
|
||||
| Indicator | **★ Wickra** | kand | ta-rs | yata |
|
||||
|------------------|------------------:|-----:|------:|-----:|
|
||||
| SMA(20) | 50 | 38 | 47 | 38 |
|
||||
| EMA(20) | 154 | 69 | 56 | 69 |
|
||||
| RSI(14) | 164 | 216 | 74 | — |
|
||||
| MACD(12, 26, 9) | 275 | 143 | 66 | — |
|
||||
| Bollinger(20, 2) | **128 ★** | 248 | 168 | — |
|
||||
| ATR(14) | 152 | 166 | 61 | — |
|
||||
|
||||
### Streaming — per-tick latency after seeding with 5 000 historical bars
|
||||
**Batch** (whole series at once). Only Wickra and kand expose a batch API;
|
||||
ta-rs and yata are streaming-only.
|
||||
|
||||
A batch-only library has to re-run its full indicator over the entire history on
|
||||
every new tick; Wickra updates state in O(1).
|
||||
| Indicator | **★ Wickra** | kand |
|
||||
|------------------|------------------:|-----:|
|
||||
| SMA(20) | 82 | 42 |
|
||||
| EMA(20) | 159 | 74 |
|
||||
| RSI(14) | **253 ★** | 274 |
|
||||
| MACD(12, 26, 9) | 681 | 283 |
|
||||
| Bollinger(20, 2) | **445 ★** | 462 |
|
||||
| ATR(14) | 175 | 173 |
|
||||
|
||||
| Indicator | **★ Wickra (per tick)** | talipp (per tick) |
|
||||
|-----------|---------------------|---------------------------|
|
||||
| RSI(14) | **0.119 µs ★** | 1.644 µs (13.8× slower) |
|
||||
ta-rs is the per-indicator speed champion on almost every row — it returns a
|
||||
bare `f64` with no warmup state and no input validation, trading away the
|
||||
`None`-warmup and NaN-safety semantics Wickra keeps. Against kand, Wickra wins
|
||||
streaming RSI, Bollinger and ATR (and batch RSI + Bollinger); Bollinger is the
|
||||
one row where Wickra is the outright fastest of all four. The leaner crates
|
||||
still win the pure recurrences (EMA, MACD) and SMA. yata exposes only SMA/EMA as
|
||||
raw-value methods, so its other rows are omitted rather than faked.
|
||||
|
||||
> TA-Lib and pandas-ta are not included here because both fail to install
|
||||
> cleanly on Windows without C build tooling — which is precisely the install
|
||||
> pain Wickra was built to remove. The benchmark script auto-detects every
|
||||
> peer library it can find and runs them on the same inputs as Wickra; install
|
||||
> them in your environment to see those rows light up too.
|
||||
### 2. Python vs the Python TA ecosystem — batch
|
||||
|
||||
Full pass over a 20 000-bar series, µs/op (lower = faster). **★** per row.
|
||||
|
||||
| Indicator | **★ Wickra** | finta | TA-Lib | tulipy |
|
||||
|------------------|------------------:|---------------------|--------|--------|
|
||||
| SMA(20) | **59.6 ★** | 354.2 (5.9× slower) | ⧗ | ⧗ |
|
||||
| EMA(20) | **88.4 ★** | 309.3 (3.5× slower) | ⧗ | ⧗ |
|
||||
| RSI(14) | **77.3 ★** | 1 283 (16.6× slower)| ⧗ | ⧗ |
|
||||
| MACD(12, 26, 9) | **116.4 ★** | 529.5 (4.6× slower) | ⧗ | ⧗ |
|
||||
| Bollinger(20, 2) | **146.0 ★** | 1 246 (8.5× slower) | ⧗ | ⧗ |
|
||||
| ATR(14) | **135.8 ★** | 3 812 (28× slower) | ⧗ | ⧗ |
|
||||
|
||||
> ⧗ = published by the CI Linux job. TA-Lib and tulipy ship C extensions that
|
||||
> don't build cleanly on every desktop, so their canonical numbers come from the
|
||||
> `cross-library-bench` workflow rather than this local table. pandas-ta needs
|
||||
> Python ≥ 3.12 and isn't in the 3.11 CI matrix. The script auto-detects
|
||||
> whichever peers are installed in your environment.
|
||||
|
||||
### 3. Python — streaming (per-tick latency)
|
||||
|
||||
Seed 5 000 bars, then feed ticks one at a time. talipp is the only Python peer
|
||||
with a true incremental API; batch-only libraries like TA-Lib must recompute the
|
||||
entire history on every tick — Wickra updates in O(1).
|
||||
|
||||
| Indicator | **★ Wickra (per tick)** | talipp (per tick) |
|
||||
|------------------|------------------------------:|-------------------------|
|
||||
| SMA(20) | **0.067 µs ★** | 0.63 µs (9.4× slower) |
|
||||
| EMA(20) | **0.051 µs ★** | 0.63 µs (12.2× slower) |
|
||||
| RSI(14) | **0.053 µs ★** | 1.00 µs (19.1× slower) |
|
||||
| MACD(12, 26, 9) | **0.071 µs ★** | 3.64 µs (51.5× slower) |
|
||||
| Bollinger(20, 2) | **0.085 µs ★** | 4.87 µs (57.2× slower) |
|
||||
|
||||
Run the suite yourself:
|
||||
|
||||
```bash
|
||||
pip install -e bindings/python[bench]
|
||||
cargo bench -p wickra-bench # Rust core vs kand / ta-rs / yata
|
||||
pip install -e bindings/python[bench] # Python peers
|
||||
python -m benchmarks.compare_libraries
|
||||
```
|
||||
|
||||
## Indicators
|
||||
|
||||
420 streaming-first indicators across twenty-four families. Every one passes the
|
||||
423 streaming-first indicators across twenty-four families. Every one passes the
|
||||
`batch == streaming` equivalence test, reference-value tests, and reset
|
||||
semantics tests. Each has a per-indicator deep dive (formula, parameters,
|
||||
warmup) at [docs.wickra.org](https://docs.wickra.org/Indicators-Overview).
|
||||
@@ -146,7 +198,7 @@ warmup) at [docs.wickra.org](https://docs.wickra.org/Indicators-Overview).
|
||||
| Moving Averages | SMA, EMA, WMA, DEMA, TEMA, HMA, KAMA, SMMA, TRIMA, ZLEMA, T3, VWMA, ALMA, McGinley Dynamic, FRAMA, VIDYA, JMA, Alligator, EVWMA, SWMA, GMA, EHMA, Median MA, Adaptive Laguerre, GD, Holt-Winters |
|
||||
| Momentum Oscillators | RSI (Wilder), Anchored RSI, Stochastic, CCI, ROC, Williams %R, MFI, Awesome Oscillator, MOM, CMO, TSI, PMO, StochRSI, Ultimate Oscillator, RVI, PGO, KST, SMI, Laguerre RSI, Connors RSI, Inertia, ROC Percentage (ROCP), ROC Ratio (ROCR), ROC Ratio 100 (ROCR100), Disparity Index, Fisher RSI, RSX, Dynamic Momentum Index, Stochastic CCI, RMI, Derivative Oscillator, Elder Ray, Intraday Momentum Index, QQE |
|
||||
| Trend & Directional | MACD, MACD Fixed (MACDFIX), MACD Extended (MACDEXT), ADX (+DI/-DI), ADXR, Aroon, TRIX, Aroon Oscillator, Vortex, Random Walk Index, Trend Intensity Index, Wave Trend Oscillator, Mass Index, Choppiness Index, Vertical Horizontal Filter, Plus DM, Minus DM, Plus DI, Minus DI, DX, TTM Trend, Trend Strength Index, Qstick, Polarized Fractal Efficiency, Wave PM, Gator Oscillator, Kase Permission Stochastic |
|
||||
| Price Oscillators | PPO, DPO, Coppock, Accelerator Oscillator, Balance of Power, APO, AO Histogram, CFO, Zero-Lag MACD, Elder Impulse, STC |
|
||||
| Price Oscillators | PPO, DPO, Coppock, Accelerator Oscillator, Balance of Power, APO, AO Histogram, CFO, Zero-Lag MACD, Elder Impulse, STC, TSF Oscillator, MACD Histogram, PPO Histogram |
|
||||
| Volatility & Bands | ATR, Bollinger Bands, Keltner Channels, Donchian Channels, NATR, StdDev, Ulcer Index, Historical Volatility, Bollinger Bandwidth, %B, True Range, Chaikin Volatility, RVI (Relative Volatility Index), Parkinson Volatility, Garman-Klass Volatility, Rogers-Satchell Volatility, Yang-Zhang Volatility |
|
||||
| Bands & Channels | MA Envelope, Acceleration Bands, STARC Bands, ATR Bands, Hurst Channel, LinReg Channel, Standard Error Bands, Double Bollinger Bands, TTM Squeeze, Fractal Chaos Bands, VWAP StdDev Bands |
|
||||
| Trailing Stops | Parabolic SAR, Parabolic SAR Extended (SAREXT), SuperTrend, Chandelier Exit, Chande Kroll Stop, ATR Trailing Stop, HiLo Activator, Volty Stop, Yo-Yo Exit, Donchian Channel Stop, Percentage Trailing Stop, Step Trailing Stop, Renko Trailing Stop |
|
||||
@@ -245,9 +297,10 @@ A Python live-trading example using the public `websockets` package lives at
|
||||
```
|
||||
wickra/
|
||||
├── crates/
|
||||
│ ├── wickra-core/ core engine + all 420 indicators
|
||||
│ ├── wickra-core/ core engine + all 423 indicators
|
||||
│ ├── wickra/ top-level facade crate (publishes on crates.io) + benches/
|
||||
│ └── wickra-data/ CSV reader, tick aggregator, live exchange feeds
|
||||
│ ├── wickra-data/ CSV reader, tick aggregator, live exchange feeds
|
||||
│ └── wickra-bench/ internal cross-library benchmark harness (not published)
|
||||
├── bindings/
|
||||
│ ├── python/ PyO3 + maturin (publishes on PyPI)
|
||||
│ ├── node/ napi-rs (publishes on npm)
|
||||
@@ -261,9 +314,10 @@ wickra/
|
||||
└── .github/workflows/ CI and release pipelines
|
||||
```
|
||||
|
||||
Rust benchmarks live in `crates/wickra/benches/`; runnable Rust examples live
|
||||
in the workspace member crate at `examples/rust/`. There is no top-level
|
||||
`benches/` directory.
|
||||
Wickra's own regression benchmarks live in `crates/wickra/benches/`; the
|
||||
cross-library comparison against kand, ta-rs and yata lives in the internal
|
||||
`crates/wickra-bench/` crate. Runnable Rust examples live in the workspace member
|
||||
crate at `examples/rust/`. There is no top-level `benches/` directory.
|
||||
|
||||
## Building everything from source
|
||||
|
||||
@@ -271,7 +325,8 @@ in the workspace member crate at `examples/rust/`. There is no top-level
|
||||
# Rust core + tests
|
||||
cargo test --workspace
|
||||
cargo clippy --workspace --all-targets -- -D warnings
|
||||
cargo bench -p wickra
|
||||
cargo bench -p wickra # Wickra's own regression benchmarks
|
||||
cargo bench -p wickra-bench # cross-library comparison (kand, ta-rs, yata)
|
||||
|
||||
# Python binding (requires Rust toolchain + maturin)
|
||||
cd bindings/python
|
||||
@@ -371,3 +426,10 @@ The library is provided **as is**, without warranty of any kind; see
|
||||
<p align="center">
|
||||
If Wickra saved you time, the cheapest way to say thanks is to ⭐ the repo.
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/wickra-lib/wickra">
|
||||
<img alt="Star Wickra on GitHub"
|
||||
src="https://img.shields.io/badge/%E2%AD%90%20Star%20Wickra%20on%20GitHub-1f2328?style=for-the-badge&logo=github&logoColor=ffd866&labelColor=1f2328">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
@@ -28,6 +28,9 @@ function num(v) {
|
||||
// --- Scalar indicators: update(value) vs batch(prices) ---
|
||||
|
||||
const scalarFactories = {
|
||||
PpoHistogram: () => new wickra.PpoHistogram(3, 6, 3),
|
||||
MacdHistogram: () => new wickra.MacdHistogram(3, 6, 3),
|
||||
TsfOscillator: () => new wickra.TsfOscillator(3),
|
||||
WAVE_PM: () => new wickra.WAVE_PM(32, 3),
|
||||
POLARIZED_FRACTAL_EFFICIENCY: () => new wickra.POLARIZED_FRACTAL_EFFICIENCY(10, 5),
|
||||
TREND_STRENGTH_INDEX: () => new wickra.TREND_STRENGTH_INDEX(20),
|
||||
|
||||
Vendored
+27
@@ -978,6 +978,15 @@ export declare class TREND_STRENGTH_INDEX {
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type TsfOscillatorNode = TsfOscillator
|
||||
export declare class TsfOscillator {
|
||||
constructor(period: number)
|
||||
update(value: number): number | null
|
||||
batch(prices: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type JumpIndicatorNode = JumpIndicator
|
||||
export declare class JumpIndicator {
|
||||
constructor(period: number, threshold: number)
|
||||
@@ -1909,6 +1918,24 @@ export declare class DerivativeOscillator {
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type MacdHistogramNode = MacdHistogram
|
||||
export declare class MacdHistogram {
|
||||
constructor(fast: number, slow: number, signal: number)
|
||||
update(value: number): number | null
|
||||
batch(prices: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type PpoHistogramNode = PpoHistogram
|
||||
export declare class PpoHistogram {
|
||||
constructor(fast: number, slow: number, signal: number)
|
||||
update(value: number): number | null
|
||||
batch(prices: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type TsiNode = TSI
|
||||
export declare class TSI {
|
||||
constructor(long: number, short: number)
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wickra-darwin-arm64",
|
||||
"version": "0.5.7",
|
||||
"version": "0.5.9",
|
||||
"description": "Native binding for wickra (macOS Apple Silicon). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||
"main": "wickra.darwin-arm64.node",
|
||||
"files": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wickra-darwin-x64",
|
||||
"version": "0.5.7",
|
||||
"version": "0.5.9",
|
||||
"description": "Native binding for wickra (macOS Intel). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||
"main": "wickra.darwin-x64.node",
|
||||
"files": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wickra-linux-arm64-gnu",
|
||||
"version": "0.5.7",
|
||||
"version": "0.5.9",
|
||||
"description": "Native binding for wickra (linux arm64 GNU). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||
"main": "wickra.linux-arm64-gnu.node",
|
||||
"files": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wickra-linux-x64-gnu",
|
||||
"version": "0.5.7",
|
||||
"version": "0.5.9",
|
||||
"description": "Native binding for wickra (linux x64 GNU). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||
"main": "wickra.linux-x64-gnu.node",
|
||||
"files": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wickra-win32-arm64-msvc",
|
||||
"version": "0.5.7",
|
||||
"version": "0.5.9",
|
||||
"description": "Native binding for wickra (Windows arm64 MSVC). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||
"main": "wickra.win32-arm64-msvc.node",
|
||||
"files": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wickra-win32-x64-msvc",
|
||||
"version": "0.5.7",
|
||||
"version": "0.5.9",
|
||||
"description": "Native binding for wickra (Windows x64 MSVC). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||
"main": "wickra.win32-x64-msvc.node",
|
||||
"files": [
|
||||
|
||||
Generated
+20
-20
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "wickra",
|
||||
"version": "0.5.7",
|
||||
"version": "0.5.9",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "wickra",
|
||||
"version": "0.5.7",
|
||||
"version": "0.5.9",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"devDependencies": {
|
||||
"@napi-rs/cli": "^2.18.0"
|
||||
@@ -15,12 +15,12 @@
|
||||
"node": ">= 18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"wickra-darwin-arm64": "0.5.7",
|
||||
"wickra-darwin-x64": "0.5.7",
|
||||
"wickra-linux-arm64-gnu": "0.5.7",
|
||||
"wickra-linux-x64-gnu": "0.5.7",
|
||||
"wickra-win32-arm64-msvc": "0.5.7",
|
||||
"wickra-win32-x64-msvc": "0.5.7"
|
||||
"wickra-darwin-arm64": "0.5.9",
|
||||
"wickra-darwin-x64": "0.5.9",
|
||||
"wickra-linux-arm64-gnu": "0.5.9",
|
||||
"wickra-linux-x64-gnu": "0.5.9",
|
||||
"wickra-win32-arm64-msvc": "0.5.9",
|
||||
"wickra-win32-x64-msvc": "0.5.9"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/cli": {
|
||||
@@ -41,8 +41,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wickra-darwin-arm64": {
|
||||
"version": "0.5.7",
|
||||
"resolved": "https://registry.npmjs.org/wickra-darwin-arm64/-/wickra-darwin-arm64-0.5.7.tgz",
|
||||
"version": "0.5.9",
|
||||
"resolved": "https://registry.npmjs.org/wickra-darwin-arm64/-/wickra-darwin-arm64-0.5.9.tgz",
|
||||
"integrity": "sha512-4eZiBR/yGUdr4nzhEUFy2i69XgNx64iI2ax/LPamsThgylC0KpHOZKK19QzJ2d9KbK4C8nMjME5FLuR+4GNEwQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
@@ -57,8 +57,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wickra-darwin-x64": {
|
||||
"version": "0.5.7",
|
||||
"resolved": "https://registry.npmjs.org/wickra-darwin-x64/-/wickra-darwin-x64-0.5.7.tgz",
|
||||
"version": "0.5.9",
|
||||
"resolved": "https://registry.npmjs.org/wickra-darwin-x64/-/wickra-darwin-x64-0.5.9.tgz",
|
||||
"integrity": "sha512-6hf8zI3QPjTFp4zCpmgUwDvNtu6jHqNUHKD5e55POo0CgA52HkpyxSPtVm8TGTIZDI7kPjlbOdBM8CJ76mmXwA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
@@ -73,8 +73,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wickra-linux-arm64-gnu": {
|
||||
"version": "0.5.7",
|
||||
"resolved": "https://registry.npmjs.org/wickra-linux-arm64-gnu/-/wickra-linux-arm64-gnu-0.5.7.tgz",
|
||||
"version": "0.5.9",
|
||||
"resolved": "https://registry.npmjs.org/wickra-linux-arm64-gnu/-/wickra-linux-arm64-gnu-0.5.9.tgz",
|
||||
"integrity": "sha512-kSe6y0xBMSiqdPLXNjwop5WZdHtvdBNKSEBCwZ4hFq33p4apW25/wrlzv9/oDuyD4kuPabJEhCCnFOplh58CUg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
@@ -89,8 +89,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wickra-linux-x64-gnu": {
|
||||
"version": "0.5.7",
|
||||
"resolved": "https://registry.npmjs.org/wickra-linux-x64-gnu/-/wickra-linux-x64-gnu-0.5.7.tgz",
|
||||
"version": "0.5.9",
|
||||
"resolved": "https://registry.npmjs.org/wickra-linux-x64-gnu/-/wickra-linux-x64-gnu-0.5.9.tgz",
|
||||
"integrity": "sha512-tWBWS4qz7hxM4xnpFb59bhf6TaLwXq0Z3jEa/2l7r8PiHA94g8r8S53NRMiT+4yiL5hSWe/nUiC/YXdRrhEZ4g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
@@ -105,8 +105,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wickra-win32-arm64-msvc": {
|
||||
"version": "0.5.7",
|
||||
"resolved": "https://registry.npmjs.org/wickra-win32-arm64-msvc/-/wickra-win32-arm64-msvc-0.5.7.tgz",
|
||||
"version": "0.5.9",
|
||||
"resolved": "https://registry.npmjs.org/wickra-win32-arm64-msvc/-/wickra-win32-arm64-msvc-0.5.9.tgz",
|
||||
"integrity": "sha512-EXIckHxAtF75PUGDKRzXyqMe9ldP0JjSdu68WFN6iJfp+McYrGu6h40TEJlQ/oUEIoPqiZB/xhVyo/el5Lg7zw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
@@ -121,8 +121,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wickra-win32-x64-msvc": {
|
||||
"version": "0.5.7",
|
||||
"resolved": "https://registry.npmjs.org/wickra-win32-x64-msvc/-/wickra-win32-x64-msvc-0.5.7.tgz",
|
||||
"version": "0.5.9",
|
||||
"resolved": "https://registry.npmjs.org/wickra-win32-x64-msvc/-/wickra-win32-x64-msvc-0.5.9.tgz",
|
||||
"integrity": "sha512-Yfsqq1Xwp6hdxMyLze411vNdo7BDwI6+lPSe7A9XdqyPecNDbtKwYLpsal2r8EHbNzqM+R8XnuRtUaEQS5VlUQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wickra",
|
||||
"version": "0.5.7",
|
||||
"version": "0.5.9",
|
||||
"description": "Streaming-first technical indicators: incremental, fast, install-free. Node bindings powered by Rust.",
|
||||
"author": "kingchenc <support@wickra.org>",
|
||||
"main": "index.js",
|
||||
@@ -47,12 +47,12 @@
|
||||
"node": ">= 18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"wickra-linux-x64-gnu": "0.5.7",
|
||||
"wickra-linux-arm64-gnu": "0.5.7",
|
||||
"wickra-darwin-x64": "0.5.7",
|
||||
"wickra-darwin-arm64": "0.5.7",
|
||||
"wickra-win32-x64-msvc": "0.5.7",
|
||||
"wickra-win32-arm64-msvc": "0.5.7"
|
||||
"wickra-linux-x64-gnu": "0.5.9",
|
||||
"wickra-linux-arm64-gnu": "0.5.9",
|
||||
"wickra-darwin-x64": "0.5.9",
|
||||
"wickra-darwin-arm64": "0.5.9",
|
||||
"wickra-win32-x64-msvc": "0.5.9",
|
||||
"wickra-win32-arm64-msvc": "0.5.9"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "napi build --platform --release",
|
||||
|
||||
@@ -219,6 +219,7 @@ node_scalar_indicator!(
|
||||
"TREND_STRENGTH_INDEX",
|
||||
wc::TrendStrengthIndex
|
||||
);
|
||||
node_scalar_indicator!(TsfOscillatorNode, "TsfOscillator", wc::TsfOscillator);
|
||||
#[napi(js_name = "JumpIndicator")]
|
||||
pub struct JumpIndicatorNode {
|
||||
inner: wc::JumpIndicator,
|
||||
@@ -4533,6 +4534,82 @@ impl DerivativeOscillatorNode {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== MacdHistogram ==============================
|
||||
|
||||
#[napi(js_name = "MacdHistogram")]
|
||||
pub struct MacdHistogramNode {
|
||||
inner: wc::MacdHistogram,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl MacdHistogramNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(fast: u32, slow: u32, signal: u32) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::MacdHistogram::new(fast as usize, slow as usize, signal as usize)
|
||||
.map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(&mut self, value: f64) -> Option<f64> {
|
||||
self.inner.update(value)
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(&mut self, prices: Vec<f64>) -> Vec<f64> {
|
||||
flatten(self.inner.batch(&prices))
|
||||
}
|
||||
#[napi]
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
#[napi(js_name = "isReady")]
|
||||
pub fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
#[napi(js_name = "warmupPeriod")]
|
||||
pub fn warmup_period(&self) -> u32 {
|
||||
self.inner.warmup_period() as u32
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== PpoHistogram ==============================
|
||||
|
||||
#[napi(js_name = "PpoHistogram")]
|
||||
pub struct PpoHistogramNode {
|
||||
inner: wc::PpoHistogram,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl PpoHistogramNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(fast: u32, slow: u32, signal: u32) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::PpoHistogram::new(fast as usize, slow as usize, signal as usize)
|
||||
.map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(&mut self, value: f64) -> Option<f64> {
|
||||
self.inner.update(value)
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(&mut self, prices: Vec<f64>) -> Vec<f64> {
|
||||
flatten(self.inner.batch(&prices))
|
||||
}
|
||||
#[napi]
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
#[napi(js_name = "isReady")]
|
||||
pub fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
#[napi(js_name = "warmupPeriod")]
|
||||
pub fn warmup_period(&self) -> u32 {
|
||||
self.inner.warmup_period() as u32
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== TSI ==============================
|
||||
|
||||
#[napi(js_name = "TSI")]
|
||||
|
||||
@@ -49,6 +49,7 @@ TALIB = _try_import("talib")
|
||||
PANDAS_TA = _try_import("pandas_ta")
|
||||
TALIPP = _try_import("talipp.indicators") or _try_import("talipp")
|
||||
FINTA = _try_import("finta")
|
||||
TULIPY = _try_import("tulipy")
|
||||
PD = _try_import("pandas")
|
||||
import wickra as WICKRA # noqa: E402 -- the library under test must be importable
|
||||
|
||||
@@ -275,6 +276,34 @@ def talipp_bollinger_batch(prices: np.ndarray) -> Optional[Callable[[], None]]:
|
||||
return lambda: BB(period=20, std_dev_mult=2.0, input_values=list(prices))
|
||||
|
||||
|
||||
# tulipy wraps the C "Tulip Indicators" library; it takes contiguous float64
|
||||
# arrays and indicator options as positional arguments.
|
||||
|
||||
|
||||
def tulipy_sma_batch(prices: np.ndarray) -> Optional[Callable[[], None]]:
|
||||
return None if TULIPY is None else (lambda: TULIPY.sma(prices, 20))
|
||||
|
||||
|
||||
def tulipy_ema_batch(prices: np.ndarray) -> Optional[Callable[[], None]]:
|
||||
return None if TULIPY is None else (lambda: TULIPY.ema(prices, 20))
|
||||
|
||||
|
||||
def tulipy_rsi_batch(prices: np.ndarray) -> Optional[Callable[[], None]]:
|
||||
return None if TULIPY is None else (lambda: TULIPY.rsi(prices, 14))
|
||||
|
||||
|
||||
def tulipy_macd_batch(prices: np.ndarray) -> Optional[Callable[[], None]]:
|
||||
return None if TULIPY is None else (lambda: TULIPY.macd(prices, 12, 26, 9))
|
||||
|
||||
|
||||
def tulipy_bollinger_batch(prices: np.ndarray) -> Optional[Callable[[], None]]:
|
||||
return None if TULIPY is None else (lambda: TULIPY.bbands(prices, 20, 2.0))
|
||||
|
||||
|
||||
def tulipy_atr_batch(high: np.ndarray, low: np.ndarray, close: np.ndarray) -> Optional[Callable[[], None]]:
|
||||
return None if TULIPY is None else (lambda: TULIPY.atr(high, low, close, 14))
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Streaming scenario: per-tick latency
|
||||
# --------------------------------------------------------------------------- #
|
||||
@@ -329,6 +358,105 @@ def talipp_rsi_streaming(seed: np.ndarray, live: np.ndarray) -> Optional[Callabl
|
||||
return run
|
||||
|
||||
|
||||
# Scalar streaming peers: Wickra and talipp both update incrementally in O(1),
|
||||
# so this is the like-for-like per-tick comparison (batch-only libs are covered
|
||||
# by the batch tables and the recompute contrast on RSI above).
|
||||
|
||||
|
||||
def wickra_sma_streaming(seed: np.ndarray, live: np.ndarray) -> Callable[[], None]:
|
||||
def run() -> None:
|
||||
sma = WICKRA.SMA(20)
|
||||
sma.batch(seed)
|
||||
for p in live:
|
||||
sma.update(float(p))
|
||||
|
||||
return run
|
||||
|
||||
|
||||
def talipp_sma_streaming(seed: np.ndarray, live: np.ndarray) -> Optional[Callable[[], None]]:
|
||||
if TALIPP is None:
|
||||
return None
|
||||
from talipp.indicators import SMA # type: ignore
|
||||
|
||||
def run() -> None:
|
||||
sma = SMA(period=20, input_values=list(seed))
|
||||
for p in live:
|
||||
sma.add(float(p))
|
||||
|
||||
return run
|
||||
|
||||
|
||||
def wickra_ema_streaming(seed: np.ndarray, live: np.ndarray) -> Callable[[], None]:
|
||||
def run() -> None:
|
||||
ema = WICKRA.EMA(20)
|
||||
ema.batch(seed)
|
||||
for p in live:
|
||||
ema.update(float(p))
|
||||
|
||||
return run
|
||||
|
||||
|
||||
def talipp_ema_streaming(seed: np.ndarray, live: np.ndarray) -> Optional[Callable[[], None]]:
|
||||
if TALIPP is None:
|
||||
return None
|
||||
from talipp.indicators import EMA # type: ignore
|
||||
|
||||
def run() -> None:
|
||||
ema = EMA(period=20, input_values=list(seed))
|
||||
for p in live:
|
||||
ema.add(float(p))
|
||||
|
||||
return run
|
||||
|
||||
|
||||
def wickra_macd_streaming(seed: np.ndarray, live: np.ndarray) -> Callable[[], None]:
|
||||
def run() -> None:
|
||||
macd = WICKRA.MACD()
|
||||
macd.batch(seed)
|
||||
for p in live:
|
||||
macd.update(float(p))
|
||||
|
||||
return run
|
||||
|
||||
|
||||
def talipp_macd_streaming(seed: np.ndarray, live: np.ndarray) -> Optional[Callable[[], None]]:
|
||||
if TALIPP is None:
|
||||
return None
|
||||
from talipp.indicators import MACD # type: ignore
|
||||
|
||||
def run() -> None:
|
||||
macd = MACD(
|
||||
fast_period=12, slow_period=26, signal_period=9, input_values=list(seed)
|
||||
)
|
||||
for p in live:
|
||||
macd.add(float(p))
|
||||
|
||||
return run
|
||||
|
||||
|
||||
def wickra_bollinger_streaming(seed: np.ndarray, live: np.ndarray) -> Callable[[], None]:
|
||||
def run() -> None:
|
||||
bb = WICKRA.BollingerBands(20, 2.0)
|
||||
bb.batch(seed)
|
||||
for p in live:
|
||||
bb.update(float(p))
|
||||
|
||||
return run
|
||||
|
||||
|
||||
def talipp_bollinger_streaming(seed: np.ndarray, live: np.ndarray) -> Optional[Callable[[], None]]:
|
||||
if TALIPP is None:
|
||||
return None
|
||||
from talipp.indicators import BB # type: ignore
|
||||
|
||||
def run() -> None:
|
||||
bb = BB(period=20, std_dev_mult=2.0, input_values=list(seed))
|
||||
for p in live:
|
||||
bb.add(float(p))
|
||||
|
||||
return run
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Runner
|
||||
# --------------------------------------------------------------------------- #
|
||||
@@ -339,6 +467,7 @@ BATCH_INDICATORS = [
|
||||
("Wickra", wickra_sma_batch),
|
||||
("TA-Lib", talib_sma_batch),
|
||||
("pandas-ta", pandas_ta_sma_batch),
|
||||
("tulipy", tulipy_sma_batch),
|
||||
("finta", finta_sma_batch),
|
||||
("talipp", talipp_sma_batch),
|
||||
]),
|
||||
@@ -346,6 +475,7 @@ BATCH_INDICATORS = [
|
||||
("Wickra", wickra_ema_batch),
|
||||
("TA-Lib", talib_ema_batch),
|
||||
("pandas-ta", pandas_ta_ema_batch),
|
||||
("tulipy", tulipy_ema_batch),
|
||||
("finta", finta_ema_batch),
|
||||
("talipp", talipp_ema_batch),
|
||||
]),
|
||||
@@ -353,6 +483,7 @@ BATCH_INDICATORS = [
|
||||
("Wickra", wickra_rsi_batch),
|
||||
("TA-Lib", talib_rsi_batch),
|
||||
("pandas-ta", pandas_ta_rsi_batch),
|
||||
("tulipy", tulipy_rsi_batch),
|
||||
("finta", finta_rsi_batch),
|
||||
("talipp", talipp_rsi_batch),
|
||||
]),
|
||||
@@ -360,6 +491,7 @@ BATCH_INDICATORS = [
|
||||
("Wickra", wickra_macd_batch),
|
||||
("TA-Lib", talib_macd_batch),
|
||||
("pandas-ta", pandas_ta_macd_batch),
|
||||
("tulipy", tulipy_macd_batch),
|
||||
("finta", finta_macd_batch),
|
||||
("talipp", talipp_macd_batch),
|
||||
]),
|
||||
@@ -367,6 +499,7 @@ BATCH_INDICATORS = [
|
||||
("Wickra", wickra_bollinger_batch),
|
||||
("TA-Lib", talib_bollinger_batch),
|
||||
("pandas-ta", pandas_ta_bollinger_batch),
|
||||
("tulipy", tulipy_bollinger_batch),
|
||||
("finta", finta_bollinger_batch),
|
||||
("talipp", talipp_bollinger_batch),
|
||||
]),
|
||||
@@ -376,18 +509,35 @@ OHLC_INDICATORS = [
|
||||
("ATR(14)", [
|
||||
("Wickra", wickra_atr_batch),
|
||||
("TA-Lib", talib_atr_batch),
|
||||
("tulipy", tulipy_atr_batch),
|
||||
("finta", finta_atr_batch),
|
||||
("talipp", talipp_atr_batch),
|
||||
]),
|
||||
]
|
||||
|
||||
STREAMING_INDICATORS = [
|
||||
("SMA(20)", [
|
||||
("Wickra", wickra_sma_streaming),
|
||||
("talipp", talipp_sma_streaming),
|
||||
]),
|
||||
("EMA(20)", [
|
||||
("Wickra", wickra_ema_streaming),
|
||||
("talipp", talipp_ema_streaming),
|
||||
]),
|
||||
("RSI(14)", [
|
||||
("Wickra", wickra_rsi_streaming),
|
||||
("TA-Lib", talib_rsi_streaming),
|
||||
("pandas-ta", pandas_ta_rsi_streaming),
|
||||
("talipp", talipp_rsi_streaming),
|
||||
]),
|
||||
("MACD(12, 26, 9)", [
|
||||
("Wickra", wickra_macd_streaming),
|
||||
("talipp", talipp_macd_streaming),
|
||||
]),
|
||||
("Bollinger(20, 2.0)", [
|
||||
("Wickra", wickra_bollinger_streaming),
|
||||
("talipp", talipp_bollinger_streaming),
|
||||
]),
|
||||
]
|
||||
|
||||
|
||||
@@ -501,6 +651,7 @@ def main() -> None:
|
||||
available = []
|
||||
if TALIB is not None: available.append("TA-Lib")
|
||||
if PANDAS_TA is not None: available.append("pandas-ta")
|
||||
if TULIPY is not None: available.append("tulipy")
|
||||
if FINTA is not None: available.append("finta")
|
||||
if TALIPP is not None: available.append("talipp")
|
||||
print(f"Wickra benchmark suite — wickra=v{WICKRA.__version__}")
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "maturin"
|
||||
|
||||
[project]
|
||||
name = "wickra"
|
||||
version = "0.5.7"
|
||||
version = "0.5.9"
|
||||
description = "Streaming-first technical indicators: incremental, fast, install-free."
|
||||
readme = "README.md"
|
||||
license = "MIT OR Apache-2.0"
|
||||
@@ -39,6 +39,7 @@ bench = [
|
||||
"pytest-benchmark>=4",
|
||||
"TA-Lib; platform_system != 'Windows'",
|
||||
"pandas-ta>=0.3.14b",
|
||||
"tulipy>=0.4; platform_system != 'Windows'",
|
||||
"talipp>=2",
|
||||
"finta>=1.3",
|
||||
"pandas>=2",
|
||||
|
||||
@@ -25,6 +25,9 @@ from __future__ import annotations
|
||||
|
||||
from ._wickra import (
|
||||
__version__,
|
||||
PpoHistogram,
|
||||
MacdHistogram,
|
||||
TsfOscillator,
|
||||
Qstick,
|
||||
GatorOscillator,
|
||||
KasePermissionStochastic,
|
||||
@@ -473,6 +476,9 @@ from ._wickra import (
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"PpoHistogram",
|
||||
"MacdHistogram",
|
||||
"TsfOscillator",
|
||||
"Qstick",
|
||||
"GatorOscillator",
|
||||
"KasePermissionStochastic",
|
||||
|
||||
@@ -3295,6 +3295,144 @@ impl PyKasePermissionStochastic {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== TsfOscillator ==============================
|
||||
|
||||
#[pyclass(name = "TsfOscillator", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyTsfOscillator {
|
||||
inner: wc::TsfOscillator,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyTsfOscillator {
|
||||
#[new]
|
||||
#[pyo3(signature = (period=14))]
|
||||
fn new(period: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::TsfOscillator::new(period).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
fn update(&mut self, value: f64) -> Option<f64> {
|
||||
self.inner.update(value)
|
||||
}
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
prices: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
let s = prices
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
Ok(flatten(self.inner.batch(s)).into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn period(&self) -> usize {
|
||||
self.inner.period()
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.inner.warmup_period()
|
||||
}
|
||||
fn __repr__(&self) -> String {
|
||||
format!("TsfOscillator(period={})", self.inner.period())
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== MacdHistogram ==============================
|
||||
|
||||
#[pyclass(name = "MacdHistogram", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyMacdHistogram {
|
||||
inner: wc::MacdHistogram,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyMacdHistogram {
|
||||
#[new]
|
||||
#[pyo3(signature = (fast=12, slow=26, signal=9))]
|
||||
fn new(fast: usize, slow: usize, signal: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::MacdHistogram::new(fast, slow, signal).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
fn update(&mut self, value: f64) -> Option<f64> {
|
||||
self.inner.update(value)
|
||||
}
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
prices: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
let s = prices
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
Ok(flatten(self.inner.batch(s)).into_pyarray(py))
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.inner.warmup_period()
|
||||
}
|
||||
fn __repr__(&self) -> String {
|
||||
let (fast, slow, signal) = self.inner.periods();
|
||||
format!("MacdHistogram(fast={fast}, slow={slow}, signal={signal})")
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== PpoHistogram ==============================
|
||||
|
||||
#[pyclass(name = "PpoHistogram", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyPpoHistogram {
|
||||
inner: wc::PpoHistogram,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyPpoHistogram {
|
||||
#[new]
|
||||
#[pyo3(signature = (fast=12, slow=26, signal=9))]
|
||||
fn new(fast: usize, slow: usize, signal: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::PpoHistogram::new(fast, slow, signal).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
fn update(&mut self, value: f64) -> Option<f64> {
|
||||
self.inner.update(value)
|
||||
}
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
prices: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
let s = prices
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
Ok(flatten(self.inner.batch(s)).into_pyarray(py))
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.inner.warmup_period()
|
||||
}
|
||||
fn __repr__(&self) -> String {
|
||||
let (fast, slow, signal) = self.inner.periods();
|
||||
format!("PpoHistogram(fast={fast}, slow={slow}, signal={signal})")
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Stochastic ==============================
|
||||
|
||||
#[pyclass(name = "IMI", module = "wickra._wickra", skip_from_py_object)]
|
||||
@@ -21422,5 +21560,8 @@ fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_class::<PyWavePm>()?;
|
||||
m.add_class::<PyGatorOscillator>()?;
|
||||
m.add_class::<PyKasePermissionStochastic>()?;
|
||||
m.add_class::<PyTsfOscillator>()?;
|
||||
m.add_class::<PyMacdHistogram>()?;
|
||||
m.add_class::<PyPpoHistogram>()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -45,6 +45,9 @@ def ohlcv() -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
|
||||
# --- Scalar (f64 -> f64) indicators ---------------------------------------
|
||||
|
||||
SCALAR = [
|
||||
(ta.PpoHistogram, (3, 6, 3)),
|
||||
(ta.MacdHistogram, (3, 6, 3)),
|
||||
(ta.TsfOscillator, (3,)),
|
||||
(ta.WAVE_PM, (32, 3)),
|
||||
(ta.POLARIZED_FRACTAL_EFFICIENCY, (10, 5)),
|
||||
(ta.TREND_STRENGTH_INDEX, (20,)),
|
||||
@@ -2862,6 +2865,31 @@ def test_kase_permission_stochastic_reference():
|
||||
assert out[-1][0] == pytest.approx(50.0)
|
||||
assert out[-1][1] == pytest.approx(50.0)
|
||||
|
||||
|
||||
def test_tsf_oscillator_reference():
|
||||
t = ta.TsfOscillator(3)
|
||||
assert t.update(1.0) is None
|
||||
assert t.update(2.0) is None
|
||||
assert t.update(9.0) == pytest.approx(-33.33333333333333)
|
||||
|
||||
|
||||
def test_macd_histogram_reference():
|
||||
# On a constant-slope ramp the MACD line is flat once seeded, so the
|
||||
# signal EMA catches up and the histogram collapses to 0.
|
||||
t = ta.MacdHistogram(3, 6, 3)
|
||||
for i in range(7):
|
||||
assert t.update(100.0 + i * 2.0) is None
|
||||
assert t.update(100.0 + 7 * 2.0) == pytest.approx(0.0, abs=1e-9)
|
||||
|
||||
|
||||
def test_ppo_histogram_reference():
|
||||
# PPO divides the EMA gap by the slow EMA, so on the same ramp the ratio
|
||||
# keeps drifting and the histogram stays non-zero.
|
||||
t = ta.PpoHistogram(3, 6, 3)
|
||||
for i in range(7):
|
||||
assert t.update(100.0 + i * 2.0) is None
|
||||
assert t.update(100.0 + 7 * 2.0) == pytest.approx(-0.052098, abs=1e-6)
|
||||
|
||||
# --- Lifecycle ------------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
@@ -10653,6 +10653,9 @@ wasm_scalar_indicator!(WasmDynamicMomentumIndex, "DynamicMomentumIndex", wc::Dyn
|
||||
wasm_scalar_indicator!(WasmRmi, "RMI", wc::Rmi, period: usize, momentum: usize);
|
||||
wasm_scalar_indicator!(WasmDerivativeOscillator, "DerivativeOscillator", wc::DerivativeOscillator, rsi_period: usize, smooth1: usize, smooth2: usize, signal_period: usize);
|
||||
wasm_scalar_indicator!(WasmTrendStrengthIndex, "TREND_STRENGTH_INDEX", wc::TrendStrengthIndex, period: usize);
|
||||
wasm_scalar_indicator!(WasmTsfOscillator, "TsfOscillator", wc::TsfOscillator, period: usize);
|
||||
wasm_scalar_indicator!(WasmMacdHistogram, "MacdHistogram", wc::MacdHistogram, fast: usize, slow: usize, signal: usize);
|
||||
wasm_scalar_indicator!(WasmPpoHistogram, "PpoHistogram", wc::PpoHistogram, fast: usize, slow: usize, signal: usize);
|
||||
|
||||
// --- DrawdownDuration: u32 output, no constructor args ---
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "wickra-bench"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
publish = false
|
||||
description = "Internal cross-library benchmark harness (not published)."
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
wickra = { path = "../wickra" }
|
||||
wickra-data = { path = "../wickra-data" }
|
||||
criterion = { workspace = true }
|
||||
kand = "0.2.2"
|
||||
ta = "0.5.0"
|
||||
yata = "0.7.0"
|
||||
|
||||
[[bench]]
|
||||
name = "cross_lib"
|
||||
harness = false
|
||||
@@ -0,0 +1,695 @@
|
||||
//! Cross-library Criterion benchmark: Wickra vs `kand` vs `ta` (ta-rs) vs `yata`.
|
||||
//!
|
||||
//! All four are pure-Rust technical-analysis crates, so this is a like-for-like
|
||||
//! Rust-vs-Rust comparison with no language-binding overhead. It feeds the exact
|
||||
//! same BTCUSDT 1-minute candle series used by `crates/wickra/benches/indicators.rs`.
|
||||
//!
|
||||
//! Two arenas, kept honest:
|
||||
//!
|
||||
//! * **Streaming** (`*/stream`): one value fed at a time. Wickra (`Indicator::update`),
|
||||
//! ta-rs (`Next::next`) and yata (`Method::next`) carry their own state; `kand`
|
||||
//! exposes stateless `*_inc` helpers, so the per-tick state is threaded manually
|
||||
//! here, seeded from `kand`'s own batch output (the seed is computed outside the
|
||||
//! timed closure). yata only appears for SMA/EMA — its RSI/MACD/Bollinger/ATR are
|
||||
//! exposed through a heavier signal-oriented indicator API, not a raw-value method,
|
||||
//! so they are intentionally left out rather than compared unfairly.
|
||||
//! * **Batch** (`*/batch`): the whole series at once. Only Wickra (`BatchExt::batch`)
|
||||
//! and `kand` (TA-Lib-style fill-the-output-slice functions) have a real batch API;
|
||||
//! ta-rs and yata are streaming-only and are deliberately absent from this arena.
|
||||
//!
|
||||
//! Run: `cargo bench -p wickra-bench`
|
||||
|
||||
// Each indicator's benchmark group spells out every library arm explicitly, which
|
||||
// runs a few groups over the 100-line lint threshold; that verbosity is the point.
|
||||
#![allow(clippy::too_many_lines)]
|
||||
|
||||
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
|
||||
use std::hint::black_box;
|
||||
use wickra::{Atr, BatchExt, BollingerBands, Candle, Ema, Indicator, MacdIndicator, Rsi, Sma};
|
||||
use wickra_data::csv::CandleReader;
|
||||
use yata::prelude::Method;
|
||||
|
||||
const SIZES: &[usize] = &[1_000, 10_000, 50_000];
|
||||
|
||||
const SMA_PERIOD: usize = 20;
|
||||
const EMA_PERIOD: usize = 20;
|
||||
const RSI_PERIOD: usize = 14;
|
||||
const ATR_PERIOD: usize = 14;
|
||||
const BB_PERIOD: usize = 20;
|
||||
const BB_DEV: f64 = 2.0;
|
||||
const MACD_FAST: usize = 12;
|
||||
const MACD_SLOW: usize = 26;
|
||||
const MACD_SIGNAL: usize = 9;
|
||||
|
||||
fn load_candles() -> Vec<Candle> {
|
||||
let path = concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/../../examples/data/btcusdt-1m.csv"
|
||||
);
|
||||
CandleReader::open(path)
|
||||
.expect("dataset present")
|
||||
.read_all()
|
||||
.expect("valid OHLCV rows")
|
||||
}
|
||||
|
||||
/// Mean of the first `period` samples — the warmup seed for `kand`'s SMA/EMA `*_inc`.
|
||||
fn window_mean(series: &[f64], period: usize) -> f64 {
|
||||
series[..period].iter().sum::<f64>() / period as f64
|
||||
}
|
||||
|
||||
fn sma_group(crit: &mut Criterion, closes: &[f64]) {
|
||||
let mut group = crit.benchmark_group("sma_20");
|
||||
for &len in SIZES {
|
||||
let len = len.min(closes.len());
|
||||
let series: &[f64] = &closes[..len];
|
||||
group.throughput(Throughput::Elements(len as u64));
|
||||
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("wickra/stream", len),
|
||||
&series,
|
||||
|bencher, &series| {
|
||||
bencher.iter(|| {
|
||||
let mut ind = Sma::new(SMA_PERIOD).unwrap();
|
||||
for &price in series {
|
||||
black_box(ind.update(price));
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("wickra/batch", len),
|
||||
&series,
|
||||
|bencher, &series| {
|
||||
bencher.iter(|| {
|
||||
let mut ind = Sma::new(SMA_PERIOD).unwrap();
|
||||
black_box(ind.batch(series));
|
||||
});
|
||||
},
|
||||
);
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("kand/stream", len),
|
||||
&series,
|
||||
|bencher, &series| {
|
||||
let seed = window_mean(series, SMA_PERIOD);
|
||||
bencher.iter(|| {
|
||||
let mut prev = seed;
|
||||
for idx in SMA_PERIOD..series.len() {
|
||||
prev = kand::ohlcv::sma::sma_inc(
|
||||
prev,
|
||||
series[idx],
|
||||
series[idx - SMA_PERIOD],
|
||||
SMA_PERIOD,
|
||||
)
|
||||
.unwrap();
|
||||
black_box(prev);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("kand/batch", len),
|
||||
&series,
|
||||
|bencher, &series| {
|
||||
bencher.iter(|| {
|
||||
let mut out = vec![0.0; series.len()];
|
||||
kand::ohlcv::sma::sma(series, SMA_PERIOD, &mut out).unwrap();
|
||||
black_box(&out);
|
||||
});
|
||||
},
|
||||
);
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("ta-rs/stream", len),
|
||||
&series,
|
||||
|bencher, &series| {
|
||||
bencher.iter(|| {
|
||||
let mut ind = ta::indicators::SimpleMovingAverage::new(SMA_PERIOD).unwrap();
|
||||
for &price in series {
|
||||
black_box(ta::Next::next(&mut ind, price));
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("yata/stream", len),
|
||||
&series,
|
||||
|bencher, &series| {
|
||||
bencher.iter(|| {
|
||||
let mut ind = yata::methods::SMA::new(SMA_PERIOD as u8, &series[0]).unwrap();
|
||||
for price in series {
|
||||
black_box(ind.next(price));
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn ema_group(crit: &mut Criterion, closes: &[f64]) {
|
||||
let mut group = crit.benchmark_group("ema_20");
|
||||
for &len in SIZES {
|
||||
let len = len.min(closes.len());
|
||||
let series: &[f64] = &closes[..len];
|
||||
group.throughput(Throughput::Elements(len as u64));
|
||||
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("wickra/stream", len),
|
||||
&series,
|
||||
|bencher, &series| {
|
||||
bencher.iter(|| {
|
||||
let mut ind = Ema::new(EMA_PERIOD).unwrap();
|
||||
for &price in series {
|
||||
black_box(ind.update(price));
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("wickra/batch", len),
|
||||
&series,
|
||||
|bencher, &series| {
|
||||
bencher.iter(|| {
|
||||
let mut ind = Ema::new(EMA_PERIOD).unwrap();
|
||||
black_box(ind.batch(series));
|
||||
});
|
||||
},
|
||||
);
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("kand/stream", len),
|
||||
&series,
|
||||
|bencher, &series| {
|
||||
let seed = window_mean(series, EMA_PERIOD);
|
||||
bencher.iter(|| {
|
||||
let mut prev = seed;
|
||||
for &price in &series[EMA_PERIOD..] {
|
||||
prev = kand::ohlcv::ema::ema_inc(price, prev, EMA_PERIOD, None).unwrap();
|
||||
black_box(prev);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("kand/batch", len),
|
||||
&series,
|
||||
|bencher, &series| {
|
||||
bencher.iter(|| {
|
||||
let mut out = vec![0.0; series.len()];
|
||||
kand::ohlcv::ema::ema(series, EMA_PERIOD, None, &mut out).unwrap();
|
||||
black_box(&out);
|
||||
});
|
||||
},
|
||||
);
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("ta-rs/stream", len),
|
||||
&series,
|
||||
|bencher, &series| {
|
||||
bencher.iter(|| {
|
||||
let mut ind =
|
||||
ta::indicators::ExponentialMovingAverage::new(EMA_PERIOD).unwrap();
|
||||
for &price in series {
|
||||
black_box(ta::Next::next(&mut ind, price));
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("yata/stream", len),
|
||||
&series,
|
||||
|bencher, &series| {
|
||||
bencher.iter(|| {
|
||||
let mut ind = yata::methods::EMA::new(EMA_PERIOD as u8, &series[0]).unwrap();
|
||||
for price in series {
|
||||
black_box(ind.next(price));
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn rsi_group(crit: &mut Criterion, closes: &[f64]) {
|
||||
let mut group = crit.benchmark_group("rsi_14");
|
||||
for &len in SIZES {
|
||||
let len = len.min(closes.len());
|
||||
let series: &[f64] = &closes[..len];
|
||||
group.throughput(Throughput::Elements(len as u64));
|
||||
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("wickra/stream", len),
|
||||
&series,
|
||||
|bencher, &series| {
|
||||
bencher.iter(|| {
|
||||
let mut ind = Rsi::new(RSI_PERIOD).unwrap();
|
||||
for &price in series {
|
||||
black_box(ind.update(price));
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("wickra/batch", len),
|
||||
&series,
|
||||
|bencher, &series| {
|
||||
bencher.iter(|| {
|
||||
let mut ind = Rsi::new(RSI_PERIOD).unwrap();
|
||||
black_box(ind.batch(series));
|
||||
});
|
||||
},
|
||||
);
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("kand/stream", len),
|
||||
&series,
|
||||
|bencher, &series| {
|
||||
// Wilder seed: simple average of the first `period` gains and losses.
|
||||
let mut gain = 0.0;
|
||||
let mut loss = 0.0;
|
||||
for idx in 1..=RSI_PERIOD {
|
||||
let delta = series[idx] - series[idx - 1];
|
||||
if delta > 0.0 {
|
||||
gain += delta;
|
||||
} else {
|
||||
loss -= delta;
|
||||
}
|
||||
}
|
||||
let seed_gain = gain / RSI_PERIOD as f64;
|
||||
let seed_loss = loss / RSI_PERIOD as f64;
|
||||
bencher.iter(|| {
|
||||
let mut avg_gain = seed_gain;
|
||||
let mut avg_loss = seed_loss;
|
||||
let mut prev_price = series[RSI_PERIOD];
|
||||
for &price in &series[RSI_PERIOD + 1..] {
|
||||
let (rsi, next_gain, next_loss) = kand::ohlcv::rsi::rsi_inc(
|
||||
price, prev_price, avg_gain, avg_loss, RSI_PERIOD,
|
||||
)
|
||||
.unwrap();
|
||||
avg_gain = next_gain;
|
||||
avg_loss = next_loss;
|
||||
prev_price = price;
|
||||
black_box(rsi);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("kand/batch", len),
|
||||
&series,
|
||||
|bencher, &series| {
|
||||
bencher.iter(|| {
|
||||
let mut rsi = vec![0.0; series.len()];
|
||||
let mut avg_gain = vec![0.0; series.len()];
|
||||
let mut avg_loss = vec![0.0; series.len()];
|
||||
kand::ohlcv::rsi::rsi(
|
||||
series,
|
||||
RSI_PERIOD,
|
||||
&mut rsi,
|
||||
&mut avg_gain,
|
||||
&mut avg_loss,
|
||||
)
|
||||
.unwrap();
|
||||
black_box(&rsi);
|
||||
});
|
||||
},
|
||||
);
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("ta-rs/stream", len),
|
||||
&series,
|
||||
|bencher, &series| {
|
||||
bencher.iter(|| {
|
||||
let mut ind = ta::indicators::RelativeStrengthIndex::new(RSI_PERIOD).unwrap();
|
||||
for &price in series {
|
||||
black_box(ta::Next::next(&mut ind, price));
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn macd_group(crit: &mut Criterion, closes: &[f64]) {
|
||||
let mut group = crit.benchmark_group("macd_12_26_9");
|
||||
for &len in SIZES {
|
||||
let len = len.min(closes.len());
|
||||
let series: &[f64] = &closes[..len];
|
||||
group.throughput(Throughput::Elements(len as u64));
|
||||
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("wickra/stream", len),
|
||||
&series,
|
||||
|bencher, &series| {
|
||||
bencher.iter(|| {
|
||||
let mut ind = MacdIndicator::classic();
|
||||
for &price in series {
|
||||
black_box(ind.update(price));
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("wickra/batch", len),
|
||||
&series,
|
||||
|bencher, &series| {
|
||||
bencher.iter(|| {
|
||||
let mut ind = MacdIndicator::classic();
|
||||
black_box(ind.batch(series));
|
||||
});
|
||||
},
|
||||
);
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("kand/stream", len),
|
||||
&series,
|
||||
|bencher, &series| {
|
||||
// Seed the fast/slow/signal EMAs from kand's own warmed-up batch state.
|
||||
let lookback =
|
||||
kand::ohlcv::macd::lookback(MACD_FAST, MACD_SLOW, MACD_SIGNAL).unwrap();
|
||||
let mut macd_line = vec![0.0; series.len()];
|
||||
let mut signal_line = vec![0.0; series.len()];
|
||||
let mut histogram = vec![0.0; series.len()];
|
||||
let mut fast_ema = vec![0.0; series.len()];
|
||||
let mut slow_ema = vec![0.0; series.len()];
|
||||
kand::ohlcv::macd::macd(
|
||||
series,
|
||||
MACD_FAST,
|
||||
MACD_SLOW,
|
||||
MACD_SIGNAL,
|
||||
&mut macd_line,
|
||||
&mut signal_line,
|
||||
&mut histogram,
|
||||
&mut fast_ema,
|
||||
&mut slow_ema,
|
||||
)
|
||||
.unwrap();
|
||||
let seed_fast = fast_ema[lookback];
|
||||
let seed_slow = slow_ema[lookback];
|
||||
let seed_signal = signal_line[lookback];
|
||||
bencher.iter(|| {
|
||||
// macd_inc returns (macd, signal, hist) but not the new EMAs, so the
|
||||
// fast/slow/signal state is threaded with kand's own ema_inc primitive.
|
||||
let mut prev_fast = seed_fast;
|
||||
let mut prev_slow = seed_slow;
|
||||
let mut prev_signal = seed_signal;
|
||||
for &price in &series[lookback + 1..] {
|
||||
let fast =
|
||||
kand::ohlcv::ema::ema_inc(price, prev_fast, MACD_FAST, None).unwrap();
|
||||
let slow =
|
||||
kand::ohlcv::ema::ema_inc(price, prev_slow, MACD_SLOW, None).unwrap();
|
||||
let macd = fast - slow;
|
||||
let signal =
|
||||
kand::ohlcv::ema::ema_inc(macd, prev_signal, MACD_SIGNAL, None)
|
||||
.unwrap();
|
||||
prev_fast = fast;
|
||||
prev_slow = slow;
|
||||
prev_signal = signal;
|
||||
black_box((macd, signal, macd - signal));
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("kand/batch", len),
|
||||
&series,
|
||||
|bencher, &series| {
|
||||
bencher.iter(|| {
|
||||
let mut macd_line = vec![0.0; series.len()];
|
||||
let mut signal_line = vec![0.0; series.len()];
|
||||
let mut histogram = vec![0.0; series.len()];
|
||||
let mut fast_ema = vec![0.0; series.len()];
|
||||
let mut slow_ema = vec![0.0; series.len()];
|
||||
kand::ohlcv::macd::macd(
|
||||
series,
|
||||
MACD_FAST,
|
||||
MACD_SLOW,
|
||||
MACD_SIGNAL,
|
||||
&mut macd_line,
|
||||
&mut signal_line,
|
||||
&mut histogram,
|
||||
&mut fast_ema,
|
||||
&mut slow_ema,
|
||||
)
|
||||
.unwrap();
|
||||
black_box(&macd_line);
|
||||
});
|
||||
},
|
||||
);
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("ta-rs/stream", len),
|
||||
&series,
|
||||
|bencher, &series| {
|
||||
bencher.iter(|| {
|
||||
let mut ind = ta::indicators::MovingAverageConvergenceDivergence::new(
|
||||
MACD_FAST,
|
||||
MACD_SLOW,
|
||||
MACD_SIGNAL,
|
||||
)
|
||||
.unwrap();
|
||||
for &price in series {
|
||||
black_box(ta::Next::next(&mut ind, price));
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn bbands_group(crit: &mut Criterion, closes: &[f64]) {
|
||||
let mut group = crit.benchmark_group("bollinger_20_2");
|
||||
for &len in SIZES {
|
||||
let len = len.min(closes.len());
|
||||
let series: &[f64] = &closes[..len];
|
||||
group.throughput(Throughput::Elements(len as u64));
|
||||
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("wickra/stream", len),
|
||||
&series,
|
||||
|bencher, &series| {
|
||||
bencher.iter(|| {
|
||||
let mut ind = BollingerBands::new(BB_PERIOD, BB_DEV).unwrap();
|
||||
for &price in series {
|
||||
black_box(ind.update(price));
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("wickra/batch", len),
|
||||
&series,
|
||||
|bencher, &series| {
|
||||
bencher.iter(|| {
|
||||
let mut ind = BollingerBands::new(BB_PERIOD, BB_DEV).unwrap();
|
||||
black_box(ind.batch(series));
|
||||
});
|
||||
},
|
||||
);
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("kand/stream", len),
|
||||
&series,
|
||||
|bencher, &series| {
|
||||
// Seed running sma/sum/sum_sq from kand's batch state at the warmup edge.
|
||||
let mut upper = vec![0.0; series.len()];
|
||||
let mut middle = vec![0.0; series.len()];
|
||||
let mut lower = vec![0.0; series.len()];
|
||||
let mut sma = vec![0.0; series.len()];
|
||||
let mut variance = vec![0.0; series.len()];
|
||||
let mut sum = vec![0.0; series.len()];
|
||||
let mut sum_sq = vec![0.0; series.len()];
|
||||
kand::ohlcv::bbands::bbands(
|
||||
series,
|
||||
BB_PERIOD,
|
||||
BB_DEV,
|
||||
BB_DEV,
|
||||
&mut upper,
|
||||
&mut middle,
|
||||
&mut lower,
|
||||
&mut sma,
|
||||
&mut variance,
|
||||
&mut sum,
|
||||
&mut sum_sq,
|
||||
)
|
||||
.unwrap();
|
||||
let seed_sma = sma[BB_PERIOD - 1];
|
||||
let seed_sum = sum[BB_PERIOD - 1];
|
||||
let seed_sum_sq = sum_sq[BB_PERIOD - 1];
|
||||
bencher.iter(|| {
|
||||
let mut prev_sma = seed_sma;
|
||||
let mut prev_sum = seed_sum;
|
||||
let mut prev_sum_sq = seed_sum_sq;
|
||||
for idx in BB_PERIOD..series.len() {
|
||||
let result = kand::ohlcv::bbands::bbands_inc(
|
||||
series[idx],
|
||||
prev_sma,
|
||||
prev_sum,
|
||||
prev_sum_sq,
|
||||
series[idx - BB_PERIOD],
|
||||
BB_PERIOD,
|
||||
BB_DEV,
|
||||
BB_DEV,
|
||||
)
|
||||
.unwrap();
|
||||
prev_sma = result.1;
|
||||
prev_sum = result.4;
|
||||
prev_sum_sq = result.5;
|
||||
black_box((result.0, result.1, result.2));
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("kand/batch", len),
|
||||
&series,
|
||||
|bencher, &series| {
|
||||
bencher.iter(|| {
|
||||
let mut upper = vec![0.0; series.len()];
|
||||
let mut middle = vec![0.0; series.len()];
|
||||
let mut lower = vec![0.0; series.len()];
|
||||
let mut sma = vec![0.0; series.len()];
|
||||
let mut variance = vec![0.0; series.len()];
|
||||
let mut sum = vec![0.0; series.len()];
|
||||
let mut sum_sq = vec![0.0; series.len()];
|
||||
kand::ohlcv::bbands::bbands(
|
||||
series,
|
||||
BB_PERIOD,
|
||||
BB_DEV,
|
||||
BB_DEV,
|
||||
&mut upper,
|
||||
&mut middle,
|
||||
&mut lower,
|
||||
&mut sma,
|
||||
&mut variance,
|
||||
&mut sum,
|
||||
&mut sum_sq,
|
||||
)
|
||||
.unwrap();
|
||||
black_box(&upper);
|
||||
});
|
||||
},
|
||||
);
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("ta-rs/stream", len),
|
||||
&series,
|
||||
|bencher, &series| {
|
||||
bencher.iter(|| {
|
||||
let mut ind = ta::indicators::BollingerBands::new(BB_PERIOD, BB_DEV).unwrap();
|
||||
for &price in series {
|
||||
black_box(ta::Next::next(&mut ind, price));
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn atr_group(crit: &mut Criterion, candles: &[Candle]) {
|
||||
let mut group = crit.benchmark_group("atr_14");
|
||||
for &len in SIZES {
|
||||
let len = len.min(candles.len());
|
||||
let series: &[Candle] = &candles[..len];
|
||||
group.throughput(Throughput::Elements(len as u64));
|
||||
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("wickra/stream", len),
|
||||
&series,
|
||||
|bencher, &series| {
|
||||
bencher.iter(|| {
|
||||
let mut ind = Atr::new(ATR_PERIOD).unwrap();
|
||||
for &candle in series {
|
||||
black_box(ind.update(candle));
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("wickra/batch", len),
|
||||
&series,
|
||||
|bencher, &series| {
|
||||
bencher.iter(|| {
|
||||
let mut ind = Atr::new(ATR_PERIOD).unwrap();
|
||||
black_box(ind.batch(series));
|
||||
});
|
||||
},
|
||||
);
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("kand/stream", len),
|
||||
&series,
|
||||
|bencher, &series| {
|
||||
let high: Vec<f64> = series.iter().map(|candle| candle.high).collect();
|
||||
let low: Vec<f64> = series.iter().map(|candle| candle.low).collect();
|
||||
let close: Vec<f64> = series.iter().map(|candle| candle.close).collect();
|
||||
// Seed prev_atr from kand's batch ATR at the first valid index (= period).
|
||||
let mut atr_out = vec![0.0; series.len()];
|
||||
kand::ohlcv::atr::atr(&high, &low, &close, ATR_PERIOD, &mut atr_out).unwrap();
|
||||
let seed_atr = atr_out[ATR_PERIOD];
|
||||
bencher.iter(|| {
|
||||
let mut prev_atr = seed_atr;
|
||||
for idx in ATR_PERIOD + 1..series.len() {
|
||||
prev_atr = kand::ohlcv::atr::atr_inc(
|
||||
high[idx],
|
||||
low[idx],
|
||||
close[idx - 1],
|
||||
prev_atr,
|
||||
ATR_PERIOD,
|
||||
)
|
||||
.unwrap();
|
||||
black_box(prev_atr);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("kand/batch", len),
|
||||
&series,
|
||||
|bencher, &series| {
|
||||
let high: Vec<f64> = series.iter().map(|candle| candle.high).collect();
|
||||
let low: Vec<f64> = series.iter().map(|candle| candle.low).collect();
|
||||
let close: Vec<f64> = series.iter().map(|candle| candle.close).collect();
|
||||
bencher.iter(|| {
|
||||
let mut atr_out = vec![0.0; series.len()];
|
||||
kand::ohlcv::atr::atr(&high, &low, &close, ATR_PERIOD, &mut atr_out).unwrap();
|
||||
black_box(&atr_out);
|
||||
});
|
||||
},
|
||||
);
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("ta-rs/stream", len),
|
||||
&series,
|
||||
|bencher, &series| {
|
||||
let items: Vec<ta::DataItem> = series
|
||||
.iter()
|
||||
.map(|candle| {
|
||||
ta::DataItem::builder()
|
||||
.open(candle.open)
|
||||
.high(candle.high)
|
||||
.low(candle.low)
|
||||
.close(candle.close)
|
||||
.volume(candle.volume)
|
||||
.build()
|
||||
.unwrap()
|
||||
})
|
||||
.collect();
|
||||
bencher.iter(|| {
|
||||
let mut ind = ta::indicators::AverageTrueRange::new(ATR_PERIOD).unwrap();
|
||||
for item in &items {
|
||||
black_box(ta::Next::next(&mut ind, item));
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn benches(crit: &mut Criterion) {
|
||||
let candles = load_candles();
|
||||
let closes: Vec<f64> = candles.iter().map(|candle| candle.close).collect();
|
||||
sma_group(crit, &closes);
|
||||
ema_group(crit, &closes);
|
||||
rsi_group(crit, &closes);
|
||||
macd_group(crit, &closes);
|
||||
bbands_group(crit, &closes);
|
||||
atr_group(crit, &candles);
|
||||
}
|
||||
|
||||
criterion_group!(name = cross_lib; config = Criterion::default(); targets = benches);
|
||||
criterion_main!(cross_lib);
|
||||
@@ -0,0 +1,6 @@
|
||||
//! Internal cross-library benchmark harness for Wickra.
|
||||
//!
|
||||
//! This crate is `publish = false`. It exists only to host the Criterion
|
||||
//! benchmark in `benches/cross_lib.rs`, which compares Wickra against the
|
||||
//! Rust technical-analysis crates `kand`, `ta` (ta-rs) and `yata` on an
|
||||
//! identical candle series. It deliberately carries no library code.
|
||||
@@ -28,9 +28,17 @@ use crate::traits::Indicator;
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Atr {
|
||||
period: usize,
|
||||
/// `period - 1` as `f64`, precomputed for the Wilder smoothing step.
|
||||
n_minus_1: f64,
|
||||
/// `1 / period`, precomputed so the per-tick smoothing multiplies instead of
|
||||
/// divides.
|
||||
inv_period: f64,
|
||||
prev_close: Option<f64>,
|
||||
seed_buf: Vec<f64>,
|
||||
avg: Option<f64>,
|
||||
/// Smoothed ATR, valid once `seeded` is set. Bare `f64` + flag rather than
|
||||
/// `Option<f64>` so the hot recurrence avoids an enum-tag read per tick.
|
||||
avg: f64,
|
||||
seeded: bool,
|
||||
}
|
||||
|
||||
impl Atr {
|
||||
@@ -45,9 +53,12 @@ impl Atr {
|
||||
}
|
||||
Ok(Self {
|
||||
period,
|
||||
n_minus_1: (period - 1) as f64,
|
||||
inv_period: 1.0 / period as f64,
|
||||
prev_close: None,
|
||||
seed_buf: Vec::with_capacity(period),
|
||||
avg: None,
|
||||
avg: 0.0,
|
||||
seeded: false,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -58,7 +69,11 @@ impl Atr {
|
||||
|
||||
/// Current value if available.
|
||||
pub const fn value(&self) -> Option<f64> {
|
||||
self.avg
|
||||
if self.seeded {
|
||||
Some(self.avg)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,17 +85,18 @@ impl Indicator for Atr {
|
||||
let tr = candle.true_range(self.prev_close);
|
||||
self.prev_close = Some(candle.close);
|
||||
|
||||
if let Some(avg) = self.avg {
|
||||
let n = self.period as f64;
|
||||
let new_avg = avg.mul_add(n - 1.0, tr) / n;
|
||||
self.avg = Some(new_avg);
|
||||
if self.seeded {
|
||||
// Wilder smoothing with the reciprocal hoisted out of the hot path.
|
||||
let new_avg = self.avg.mul_add(self.n_minus_1, tr) * self.inv_period;
|
||||
self.avg = new_avg;
|
||||
return Some(new_avg);
|
||||
}
|
||||
|
||||
self.seed_buf.push(tr);
|
||||
if self.seed_buf.len() == self.period {
|
||||
let seed = self.seed_buf.iter().copied().sum::<f64>() / self.period as f64;
|
||||
self.avg = Some(seed);
|
||||
self.avg = seed;
|
||||
self.seeded = true;
|
||||
return Some(seed);
|
||||
}
|
||||
None
|
||||
@@ -89,7 +105,8 @@ impl Indicator for Atr {
|
||||
fn reset(&mut self) {
|
||||
self.prev_close = None;
|
||||
self.seed_buf.clear();
|
||||
self.avg = None;
|
||||
self.avg = 0.0;
|
||||
self.seeded = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
@@ -97,7 +114,7 @@ impl Indicator for Atr {
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.avg.is_some()
|
||||
self.seeded
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
//! Bollinger Bands.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::traits::Indicator;
|
||||
|
||||
@@ -49,7 +47,13 @@ pub struct BollingerOutput {
|
||||
pub struct BollingerBands {
|
||||
period: usize,
|
||||
multiplier: f64,
|
||||
window: VecDeque<f64>,
|
||||
/// Fixed-capacity ring buffer of the last `period` finite inputs. A flat
|
||||
/// `Box<[f64]>` with a manual write cursor beats `VecDeque` on this hot path.
|
||||
buf: Box<[f64]>,
|
||||
/// Index of the next slot to write — also the oldest element once full.
|
||||
head: usize,
|
||||
/// Number of slots filled, saturating at `period`.
|
||||
count: usize,
|
||||
sum: f64,
|
||||
sum_sq: f64,
|
||||
/// Number of finite updates since the running sums were last reseeded
|
||||
@@ -80,7 +84,9 @@ impl BollingerBands {
|
||||
Ok(Self {
|
||||
period,
|
||||
multiplier,
|
||||
window: VecDeque::with_capacity(period),
|
||||
buf: vec![0.0; period].into_boxed_slice(),
|
||||
head: 0,
|
||||
count: 0,
|
||||
sum: 0.0,
|
||||
sum_sq: 0.0,
|
||||
updates_since_recompute: 0,
|
||||
@@ -103,7 +109,7 @@ impl BollingerBands {
|
||||
}
|
||||
|
||||
fn current(&self) -> Option<BollingerOutput> {
|
||||
if self.window.len() != self.period {
|
||||
if self.count != self.period {
|
||||
return None;
|
||||
}
|
||||
let n = self.period as f64;
|
||||
@@ -129,25 +135,38 @@ impl Indicator for BollingerBands {
|
||||
if !input.is_finite() {
|
||||
return self.current();
|
||||
}
|
||||
if self.window.len() == self.period {
|
||||
let old = self.window.pop_front().expect("non-empty");
|
||||
if self.count == self.period {
|
||||
let old = self.buf[self.head];
|
||||
self.sum -= old;
|
||||
self.sum_sq -= old * old;
|
||||
self.buf[self.head] = input;
|
||||
self.sum += input;
|
||||
self.sum_sq += input * input;
|
||||
} else {
|
||||
self.buf[self.head] = input;
|
||||
self.sum += input;
|
||||
self.sum_sq += input * input;
|
||||
self.count += 1;
|
||||
}
|
||||
self.head += 1;
|
||||
if self.head == self.period {
|
||||
self.head = 0;
|
||||
}
|
||||
self.window.push_back(input);
|
||||
self.sum += input;
|
||||
self.sum_sq += input * input;
|
||||
self.updates_since_recompute += 1;
|
||||
if self.updates_since_recompute >= RECOMPUTE_EVERY * self.period {
|
||||
self.sum = self.window.iter().copied().sum();
|
||||
self.sum_sq = self.window.iter().copied().map(|x| x * x).sum();
|
||||
// Reseed in chronological order (oldest at `head`) to keep the running
|
||||
// sums bit-equivalent to a fresh from-scratch pass on stable inputs.
|
||||
let chronological = self.buf[self.head..].iter().chain(&self.buf[..self.head]);
|
||||
self.sum = chronological.clone().copied().sum();
|
||||
self.sum_sq = chronological.map(|&x| x * x).sum();
|
||||
self.updates_since_recompute = 0;
|
||||
}
|
||||
self.current()
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.window.clear();
|
||||
self.head = 0;
|
||||
self.count = 0;
|
||||
self.sum = 0.0;
|
||||
self.sum_sq = 0.0;
|
||||
self.updates_since_recompute = 0;
|
||||
@@ -158,7 +177,7 @@ impl Indicator for BollingerBands {
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.window.len() == self.period
|
||||
self.count == self.period
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
@@ -171,6 +190,7 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
use std::collections::VecDeque;
|
||||
|
||||
fn naive(prices: &[f64], period: usize, mult: f64) -> BollingerOutput {
|
||||
assert!(
|
||||
|
||||
@@ -25,7 +25,15 @@ use crate::traits::Indicator;
|
||||
pub struct Ema {
|
||||
period: usize,
|
||||
alpha: f64,
|
||||
state: Option<f64>,
|
||||
/// `1 - alpha`, precomputed so the recurrence avoids a subtraction per tick.
|
||||
/// Cached value, so the steady-state output is bit-for-bit unchanged.
|
||||
one_minus_alpha: f64,
|
||||
/// Latest EMA value, valid only once `seeded` is true. Stored as a bare `f64`
|
||||
/// (plus the `seeded` flag) rather than `Option<f64>` so the steady-state
|
||||
/// recurrence reads and writes 8 bytes with no enum-tag handling per tick.
|
||||
current: f64,
|
||||
/// Whether `current` holds a real value yet (warmup complete).
|
||||
seeded: bool,
|
||||
warmup_buf: Vec<f64>,
|
||||
}
|
||||
|
||||
@@ -43,7 +51,9 @@ impl Ema {
|
||||
Ok(Self {
|
||||
period,
|
||||
alpha,
|
||||
state: None,
|
||||
one_minus_alpha: 1.0 - alpha,
|
||||
current: 0.0,
|
||||
seeded: false,
|
||||
warmup_buf: Vec::with_capacity(period),
|
||||
})
|
||||
}
|
||||
@@ -66,7 +76,9 @@ impl Ema {
|
||||
Ok(Self {
|
||||
period: 1,
|
||||
alpha,
|
||||
state: None,
|
||||
one_minus_alpha: 1.0 - alpha,
|
||||
current: 0.0,
|
||||
seeded: false,
|
||||
warmup_buf: Vec::with_capacity(1),
|
||||
})
|
||||
}
|
||||
@@ -83,21 +95,28 @@ impl Ema {
|
||||
|
||||
/// Current value if available.
|
||||
pub const fn value(&self) -> Option<f64> {
|
||||
self.state
|
||||
if self.seeded {
|
||||
Some(self.current)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal helper that feeds a value without finiteness validation. The caller
|
||||
/// guarantees `input.is_finite()`. Used by MACD which has already validated.
|
||||
pub(crate) fn step_unchecked(&mut self, input: f64) -> Option<f64> {
|
||||
if let Some(prev) = self.state {
|
||||
let new = self.alpha.mul_add(input, (1.0 - self.alpha) * prev);
|
||||
self.state = Some(new);
|
||||
if self.seeded {
|
||||
let new = self
|
||||
.alpha
|
||||
.mul_add(input, self.one_minus_alpha * self.current);
|
||||
self.current = new;
|
||||
return Some(new);
|
||||
}
|
||||
self.warmup_buf.push(input);
|
||||
if self.warmup_buf.len() == self.period {
|
||||
let seed = self.warmup_buf.iter().copied().sum::<f64>() / self.period as f64;
|
||||
self.state = Some(seed);
|
||||
self.current = seed;
|
||||
self.seeded = true;
|
||||
return Some(seed);
|
||||
}
|
||||
None
|
||||
@@ -110,13 +129,14 @@ impl Indicator for Ema {
|
||||
|
||||
fn update(&mut self, input: f64) -> Option<f64> {
|
||||
if !input.is_finite() {
|
||||
return self.state;
|
||||
return self.value();
|
||||
}
|
||||
self.step_unchecked(input)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.state = None;
|
||||
self.current = 0.0;
|
||||
self.seeded = false;
|
||||
self.warmup_buf.clear();
|
||||
}
|
||||
|
||||
@@ -125,7 +145,7 @@ impl Indicator for Ema {
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.state.is_some()
|
||||
self.seeded
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
//! MACD Histogram (standalone).
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::indicators::macd::MacdIndicator;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// MACD Histogram — the `macd − signal` bar of [`MacdIndicator`] as a
|
||||
/// standalone scalar indicator.
|
||||
///
|
||||
/// ```text
|
||||
/// macd = EMA(fast) − EMA(slow)
|
||||
/// signal = EMA(macd, signal)
|
||||
/// histogram = macd − signal
|
||||
/// ```
|
||||
///
|
||||
/// The histogram is the most actively traded part of MACD: it crosses zero
|
||||
/// exactly when the MACD line crosses its signal, and its slope measures
|
||||
/// whether that momentum is accelerating or fading. This wrapper exposes just
|
||||
/// that series for pipelines that want a plain `f64` stream rather than the
|
||||
/// full [`MacdOutput`](crate::MacdOutput); for the line and signal alongside
|
||||
/// it, use [`MacdIndicator`](crate::MacdIndicator) directly.
|
||||
///
|
||||
/// Standard parameters are `fast = 12`, `slow = 26`, `signal = 9`, so the
|
||||
/// first value lands after `slow + signal − 1` inputs — exactly when
|
||||
/// [`MacdIndicator`] emits its first full output.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Indicator, MacdHistogram};
|
||||
///
|
||||
/// let mut indicator = MacdHistogram::new(12, 26, 9).unwrap();
|
||||
/// let mut last = None;
|
||||
/// for i in 0..80 {
|
||||
/// last = indicator.update(100.0 + f64::from(i));
|
||||
/// }
|
||||
/// assert!(last.is_some());
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MacdHistogram {
|
||||
macd: MacdIndicator,
|
||||
}
|
||||
|
||||
impl MacdHistogram {
|
||||
/// Construct a MACD histogram with the given periods.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::PeriodZero`] if any period is zero, and
|
||||
/// [`Error::InvalidPeriod`] if `fast >= slow`.
|
||||
pub fn new(fast: usize, slow: usize, signal: usize) -> Result<Self> {
|
||||
Ok(Self {
|
||||
macd: MacdIndicator::new(fast, slow, signal)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Default `(12, 26, 9)` configuration, matching every classical chart package.
|
||||
pub fn classic() -> Self {
|
||||
Self::new(12, 26, 9).expect("classic MACD periods are valid")
|
||||
}
|
||||
|
||||
/// Configured periods as `(fast, slow, signal)`.
|
||||
pub const fn periods(&self) -> (usize, usize, usize) {
|
||||
self.macd.periods()
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for MacdHistogram {
|
||||
type Input = f64;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, input: f64) -> Option<f64> {
|
||||
self.macd.update(input).map(|out| out.histogram)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.macd.reset();
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.macd.warmup_period()
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.macd.is_ready()
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"MacdHistogram"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::error::Error;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
#[test]
|
||||
fn rejects_invalid_periods() {
|
||||
assert!(matches!(
|
||||
MacdHistogram::new(0, 26, 9),
|
||||
Err(Error::PeriodZero)
|
||||
));
|
||||
assert!(matches!(
|
||||
MacdHistogram::new(12, 26, 0),
|
||||
Err(Error::PeriodZero)
|
||||
));
|
||||
assert!(matches!(
|
||||
MacdHistogram::new(26, 12, 9),
|
||||
Err(Error::InvalidPeriod { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let osc = MacdHistogram::classic();
|
||||
assert_eq!(osc.periods(), (12, 26, 9));
|
||||
assert_eq!(osc.name(), "MacdHistogram");
|
||||
assert_eq!(osc.warmup_period(), 26 + 9 - 1);
|
||||
assert!(!osc.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn equals_macd_histogram_field() {
|
||||
// The standalone series must be exactly MacdIndicator's histogram bar.
|
||||
let prices: Vec<f64> = (1..=120)
|
||||
.map(|i| 100.0 + (f64::from(i) * 0.25).sin() * 8.0)
|
||||
.collect();
|
||||
let hist = MacdHistogram::classic().batch(&prices);
|
||||
let full = MacdIndicator::classic().batch(&prices);
|
||||
assert_eq!(hist.len(), full.len());
|
||||
for (h, m) in hist.iter().zip(full.iter()) {
|
||||
assert_eq!(h.is_some(), m.is_some());
|
||||
if let (Some(h), Some(m)) = (h, m) {
|
||||
assert_relative_eq!(*h, m.histogram, epsilon = 1e-12);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn warmup_emits_first_value_at_warmup_period() {
|
||||
let mut osc = MacdHistogram::new(3, 6, 3).unwrap();
|
||||
let warmup = osc.warmup_period();
|
||||
assert_eq!(warmup, 6 + 3 - 1);
|
||||
for i in 1..warmup {
|
||||
assert!(osc.update(100.0 + i as f64).is_none());
|
||||
}
|
||||
assert!(osc.update(100.0 + warmup as f64).is_some());
|
||||
assert!(osc.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn constant_series_converges_to_zero() {
|
||||
let mut osc = MacdHistogram::classic();
|
||||
let out = osc.batch(&[100.0_f64; 200]);
|
||||
let last = out.iter().rev().flatten().next().expect("emits a value");
|
||||
assert_relative_eq!(*last, 0.0, epsilon = 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let prices: Vec<f64> = (1..=100)
|
||||
.map(|i| (f64::from(i) * 0.4).cos() * 10.0)
|
||||
.collect();
|
||||
let mut a = MacdHistogram::classic();
|
||||
let mut b = MacdHistogram::classic();
|
||||
assert_eq!(
|
||||
a.batch(&prices),
|
||||
prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut osc = MacdHistogram::classic();
|
||||
osc.batch(&(1..=80).map(f64::from).collect::<Vec<_>>());
|
||||
assert!(osc.is_ready());
|
||||
osc.reset();
|
||||
assert!(!osc.is_ready());
|
||||
assert_eq!(osc.update(1.0), None);
|
||||
}
|
||||
}
|
||||
@@ -214,6 +214,7 @@ mod ma_envelope;
|
||||
mod macd;
|
||||
mod macd_ext;
|
||||
mod macd_fix;
|
||||
mod macd_histogram;
|
||||
mod mama;
|
||||
mod market_facilitation_index;
|
||||
mod marubozu;
|
||||
@@ -270,6 +271,7 @@ mod pmo;
|
||||
mod point_and_figure_bars;
|
||||
mod polarized_fractal_efficiency;
|
||||
mod ppo;
|
||||
mod ppo_histogram;
|
||||
mod profit_factor;
|
||||
mod psar;
|
||||
mod pvi;
|
||||
@@ -381,6 +383,7 @@ mod triple_top_bottom;
|
||||
mod trix;
|
||||
mod true_range;
|
||||
mod tsf;
|
||||
mod tsf_oscillator;
|
||||
mod tsi;
|
||||
mod tsv;
|
||||
mod ttm_squeeze;
|
||||
@@ -565,7 +568,7 @@ pub use gain_loss_ratio::GainLossRatio;
|
||||
pub use gap_side_by_side_white::GapSideBySideWhite;
|
||||
pub use garman_klass::GarmanKlassVolatility;
|
||||
pub use gartley::Gartley;
|
||||
pub use gator_oscillator::GatorOscillator;
|
||||
pub use gator_oscillator::{GatorOscillator, GatorOscillatorOutput};
|
||||
pub use generalized_dema::GeneralizedDema;
|
||||
pub use geometric_ma::GeometricMa;
|
||||
pub use golden_pocket::{GoldenPocket, GoldenPocketOutput};
|
||||
@@ -608,7 +611,7 @@ pub use jump_indicator::JumpIndicator;
|
||||
pub use kagi_bars::{KagiBar, KagiBars};
|
||||
pub use kalman_hedge_ratio::{KalmanHedgeRatio, KalmanHedgeRatioOutput};
|
||||
pub use kama::Kama;
|
||||
pub use kase_permission_stochastic::KasePermissionStochastic;
|
||||
pub use kase_permission_stochastic::{KasePermissionStochastic, KasePermissionStochasticOutput};
|
||||
pub use kelly_criterion::KellyCriterion;
|
||||
pub use keltner::{Keltner, KeltnerOutput};
|
||||
pub use kicking::Kicking;
|
||||
@@ -634,6 +637,7 @@ pub use ma_envelope::{MaEnvelope, MaEnvelopeOutput};
|
||||
pub use macd::{MacdIndicator, MacdOutput};
|
||||
pub use macd_ext::{MaType, MacdExt};
|
||||
pub use macd_fix::MacdFix;
|
||||
pub use macd_histogram::MacdHistogram;
|
||||
pub use mama::{Mama, MamaOutput};
|
||||
pub use market_facilitation_index::MarketFacilitationIndex;
|
||||
pub use marubozu::Marubozu;
|
||||
@@ -690,6 +694,7 @@ pub use pmo::Pmo;
|
||||
pub use point_and_figure_bars::{PnfColumn, PointAndFigureBars};
|
||||
pub use polarized_fractal_efficiency::PolarizedFractalEfficiency;
|
||||
pub use ppo::Ppo;
|
||||
pub use ppo_histogram::PpoHistogram;
|
||||
pub use profit_factor::ProfitFactor;
|
||||
pub use psar::Psar;
|
||||
pub use pvi::Pvi;
|
||||
@@ -801,6 +806,7 @@ pub use triple_top_bottom::TripleTopBottom;
|
||||
pub use trix::Trix;
|
||||
pub use true_range::TrueRange;
|
||||
pub use tsf::Tsf;
|
||||
pub use tsf_oscillator::TsfOscillator;
|
||||
pub use tsi::Tsi;
|
||||
pub use tsv::Tsv;
|
||||
pub use ttm_squeeze::{TtmSqueeze, TtmSqueezeOutput};
|
||||
@@ -973,6 +979,9 @@ pub const FAMILIES: &[(&str, &[&str])] = &[
|
||||
"ZeroLagMacd",
|
||||
"ElderImpulse",
|
||||
"Stc",
|
||||
"TsfOscillator",
|
||||
"MacdHistogram",
|
||||
"PpoHistogram",
|
||||
],
|
||||
),
|
||||
(
|
||||
@@ -1414,6 +1423,6 @@ mod family_tests {
|
||||
// the actual indicator count is the early-warning signal that an
|
||||
// indicator was added without being assigned a family.
|
||||
let total: usize = FAMILIES.iter().map(|(_, ns)| ns.len()).sum();
|
||||
assert_eq!(total, 420, "FAMILIES total drifted from indicator count");
|
||||
assert_eq!(total, 423, "FAMILIES total drifted from indicator count");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
//! Percentage Price Oscillator Histogram.
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::indicators::ema::Ema;
|
||||
use crate::indicators::ppo::Ppo;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// PPO Histogram — the `ppo − signal` bar of the Percentage Price Oscillator.
|
||||
///
|
||||
/// ```text
|
||||
/// ppo = 100 · (EMA_fast − EMA_slow) / EMA_slow
|
||||
/// signal = EMA(ppo, signal_period)
|
||||
/// histogram = ppo − signal
|
||||
/// ```
|
||||
///
|
||||
/// [`Ppo`](crate::Ppo) itself only emits the percentage line; this indicator
|
||||
/// adds the classic 9-period signal EMA on top and reports the resulting
|
||||
/// zero-centered histogram. Because PPO is scale-free (the EMA gap is divided
|
||||
/// by the slow EMA), the histogram is **comparable across instruments** — a
|
||||
/// PPO histogram of `0.4` means the same relative momentum on any asset, unlike
|
||||
/// the price-unit [`MacdHistogram`](crate::MacdHistogram).
|
||||
///
|
||||
/// With Appel's defaults `fast = 12`, `slow = 26`, `signal = 9`, the first
|
||||
/// value lands after `slow + signal − 1` inputs — the point at which the slow
|
||||
/// EMA and then the signal EMA are both seeded.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Indicator, PpoHistogram};
|
||||
///
|
||||
/// let mut indicator = PpoHistogram::new(12, 26, 9).unwrap();
|
||||
/// let mut last = None;
|
||||
/// for i in 0..80 {
|
||||
/// last = indicator.update(100.0 + f64::from(i));
|
||||
/// }
|
||||
/// assert!(last.is_some());
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PpoHistogram {
|
||||
ppo: Ppo,
|
||||
signal_ema: Ema,
|
||||
signal_period: usize,
|
||||
current: Option<f64>,
|
||||
}
|
||||
|
||||
impl PpoHistogram {
|
||||
/// Construct a PPO histogram with the `fast`/`slow` EMA periods and the
|
||||
/// `signal` EMA period.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::PeriodZero`] if any period is `0`, or
|
||||
/// [`Error::InvalidPeriod`] if `fast >= slow`.
|
||||
pub fn new(fast: usize, slow: usize, signal: usize) -> Result<Self> {
|
||||
if signal == 0 {
|
||||
return Err(Error::PeriodZero);
|
||||
}
|
||||
Ok(Self {
|
||||
ppo: Ppo::new(fast, slow)?,
|
||||
signal_ema: Ema::new(signal)?,
|
||||
signal_period: signal,
|
||||
current: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Default `(12, 26, 9)` configuration.
|
||||
pub fn classic() -> Self {
|
||||
Self::new(12, 26, 9).expect("classic PPO periods are valid")
|
||||
}
|
||||
|
||||
/// Configured periods as `(fast, slow, signal)`.
|
||||
pub const fn periods(&self) -> (usize, usize, usize) {
|
||||
let (fast, slow) = self.ppo.periods();
|
||||
(fast, slow, self.signal_period)
|
||||
}
|
||||
|
||||
/// Current value if available.
|
||||
pub const fn value(&self) -> Option<f64> {
|
||||
self.current
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for PpoHistogram {
|
||||
type Input = f64;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, input: f64) -> Option<f64> {
|
||||
// Guard before touching either stage so a non-finite input never
|
||||
// advances the signal EMA on a stale, re-fed PPO value.
|
||||
if !input.is_finite() {
|
||||
return self.current;
|
||||
}
|
||||
let ppo = self.ppo.update(input)?;
|
||||
let signal = self.signal_ema.update(ppo)?;
|
||||
let histogram = ppo - signal;
|
||||
self.current = Some(histogram);
|
||||
Some(histogram)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.ppo.reset();
|
||||
self.signal_ema.reset();
|
||||
self.current = None;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
// Slow EMA seeds the PPO, then the signal EMA needs `signal − 1` more.
|
||||
self.ppo.warmup_period() + self.signal_period - 1
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.current.is_some()
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"PpoHistogram"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
#[test]
|
||||
fn rejects_invalid_periods() {
|
||||
assert!(matches!(
|
||||
PpoHistogram::new(0, 26, 9),
|
||||
Err(Error::PeriodZero)
|
||||
));
|
||||
assert!(matches!(
|
||||
PpoHistogram::new(12, 0, 9),
|
||||
Err(Error::PeriodZero)
|
||||
));
|
||||
assert!(matches!(
|
||||
PpoHistogram::new(12, 26, 0),
|
||||
Err(Error::PeriodZero)
|
||||
));
|
||||
assert!(matches!(
|
||||
PpoHistogram::new(26, 12, 9),
|
||||
Err(Error::InvalidPeriod { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let osc = PpoHistogram::classic();
|
||||
assert_eq!(osc.periods(), (12, 26, 9));
|
||||
assert_eq!(osc.name(), "PpoHistogram");
|
||||
assert_eq!(osc.warmup_period(), 26 + 9 - 1);
|
||||
assert_eq!(osc.value(), None);
|
||||
assert!(!osc.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn equals_ppo_minus_signal_ema() {
|
||||
// The histogram must equal PPO minus an EMA(signal) composed by hand.
|
||||
let prices: Vec<f64> = (1..=120)
|
||||
.map(|i| 100.0 + (f64::from(i) * 0.2).sin() * 6.0)
|
||||
.collect();
|
||||
let got = PpoHistogram::new(12, 26, 9).unwrap().batch(&prices);
|
||||
|
||||
let mut ppo = Ppo::new(12, 26).unwrap();
|
||||
let mut sig = Ema::new(9).unwrap();
|
||||
let mut expected = Vec::with_capacity(prices.len());
|
||||
for p in &prices {
|
||||
let out = ppo
|
||||
.update(*p)
|
||||
.and_then(|line| sig.update(line).map(|signal| line - signal));
|
||||
expected.push(out);
|
||||
}
|
||||
assert_eq!(got, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn warmup_emits_first_value_at_warmup_period() {
|
||||
let mut osc = PpoHistogram::new(3, 6, 3).unwrap();
|
||||
let warmup = osc.warmup_period();
|
||||
assert_eq!(warmup, 6 + 3 - 1);
|
||||
for i in 1..warmup {
|
||||
assert!(osc.update(100.0 + i as f64).is_none());
|
||||
}
|
||||
assert!(osc.update(100.0 + warmup as f64).is_some());
|
||||
assert!(osc.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn constant_series_converges_to_zero() {
|
||||
let mut osc = PpoHistogram::classic();
|
||||
let out = osc.batch(&[100.0_f64; 200]);
|
||||
let last = out.iter().rev().flatten().next().expect("emits a value");
|
||||
assert_relative_eq!(*last, 0.0, epsilon = 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_non_finite_input() {
|
||||
let mut osc = PpoHistogram::new(3, 6, 3).unwrap();
|
||||
let out = osc.batch(&(1..=40).map(f64::from).collect::<Vec<_>>());
|
||||
let before = *out.last().unwrap();
|
||||
assert!(before.is_some());
|
||||
assert_eq!(osc.update(f64::NAN), before);
|
||||
assert_eq!(osc.update(f64::INFINITY), before);
|
||||
assert_eq!(osc.value(), before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let prices: Vec<f64> = (1..=100)
|
||||
.map(|i| 100.0 + (f64::from(i) * 0.4).cos() * 10.0)
|
||||
.collect();
|
||||
let mut a = PpoHistogram::classic();
|
||||
let mut b = PpoHistogram::classic();
|
||||
assert_eq!(
|
||||
a.batch(&prices),
|
||||
prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut osc = PpoHistogram::classic();
|
||||
osc.batch(&(1..=80).map(f64::from).collect::<Vec<_>>());
|
||||
assert!(osc.is_ready());
|
||||
osc.reset();
|
||||
assert!(!osc.is_ready());
|
||||
assert_eq!(osc.update(1.0), None);
|
||||
}
|
||||
}
|
||||
@@ -25,13 +25,24 @@ use crate::traits::Indicator;
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Rsi {
|
||||
period: usize,
|
||||
prev_close: Option<f64>,
|
||||
/// `period - 1` as `f64`, precomputed for the Wilder smoothing step.
|
||||
n_minus_1: f64,
|
||||
/// `1 / period`, precomputed so the per-tick smoothing multiplies instead of
|
||||
/// divides (a reciprocal is hoisted out of the hot path).
|
||||
inv_period: f64,
|
||||
/// Previous close, valid once `has_prev` is set. Bare `f64` + flag instead of
|
||||
/// `Option<f64>` to avoid an enum-tag read on every tick.
|
||||
prev_close: f64,
|
||||
has_prev: bool,
|
||||
// Wilder seeds with the simple average of the first `period` gains/losses,
|
||||
// then transitions to recursive smoothing.
|
||||
seed_buf_gains: Vec<f64>,
|
||||
seed_buf_losses: Vec<f64>,
|
||||
avg_gain: Option<f64>,
|
||||
avg_loss: Option<f64>,
|
||||
/// Smoothed average gain / loss, valid once `avgs_seeded` is set. Bare `f64`s
|
||||
/// + flag so the hot recurrence avoids reading two `Option<f64>` tags per tick.
|
||||
avg_gain: f64,
|
||||
avg_loss: f64,
|
||||
avgs_seeded: bool,
|
||||
last_value: Option<f64>,
|
||||
}
|
||||
|
||||
@@ -47,11 +58,15 @@ impl Rsi {
|
||||
}
|
||||
Ok(Self {
|
||||
period,
|
||||
prev_close: None,
|
||||
n_minus_1: (period - 1) as f64,
|
||||
inv_period: 1.0 / period as f64,
|
||||
prev_close: 0.0,
|
||||
has_prev: false,
|
||||
seed_buf_gains: Vec::with_capacity(period),
|
||||
seed_buf_losses: Vec::with_capacity(period),
|
||||
avg_gain: None,
|
||||
avg_loss: None,
|
||||
avg_gain: 0.0,
|
||||
avg_loss: 0.0,
|
||||
avgs_seeded: false,
|
||||
last_value: None,
|
||||
})
|
||||
}
|
||||
@@ -67,16 +82,16 @@ impl Rsi {
|
||||
}
|
||||
|
||||
fn rsi_from_avgs(avg_gain: f64, avg_loss: f64) -> f64 {
|
||||
if avg_loss == 0.0 {
|
||||
if avg_gain == 0.0 {
|
||||
// No movement at all -> RSI undefined; standard convention returns 50.
|
||||
50.0
|
||||
} else {
|
||||
100.0
|
||||
}
|
||||
// Algebraically `100 - 100/(1 + ag/al)` collapses to `100·ag/(ag+al)`,
|
||||
// which needs a single division instead of two and removes the separate
|
||||
// `rs` step. Edge cases stay exact: `al == 0, ag > 0` gives `100·ag/ag =
|
||||
// 100`; `ag == 0, al > 0` gives `0`; both zero (no movement) is the
|
||||
// undefined case and returns the neutral 50.
|
||||
let denom = avg_gain + avg_loss;
|
||||
if denom == 0.0 {
|
||||
50.0
|
||||
} else {
|
||||
let rs = avg_gain / avg_loss;
|
||||
100.0 - 100.0 / (1.0 + rs)
|
||||
100.0 * avg_gain / denom
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -90,22 +105,25 @@ impl Indicator for Rsi {
|
||||
return self.last_value;
|
||||
}
|
||||
|
||||
let Some(prev) = self.prev_close else {
|
||||
self.prev_close = Some(input);
|
||||
if !self.has_prev {
|
||||
self.prev_close = input;
|
||||
self.has_prev = true;
|
||||
return None;
|
||||
};
|
||||
self.prev_close = Some(input);
|
||||
}
|
||||
let prev = self.prev_close;
|
||||
self.prev_close = input;
|
||||
|
||||
let diff = input - prev;
|
||||
let gain = if diff > 0.0 { diff } else { 0.0 };
|
||||
let loss = if diff < 0.0 { -diff } else { 0.0 };
|
||||
|
||||
if let (Some(ag), Some(al)) = (self.avg_gain, self.avg_loss) {
|
||||
let n = self.period as f64;
|
||||
let new_ag = (ag * (n - 1.0) + gain) / n;
|
||||
let new_al = (al * (n - 1.0) + loss) / n;
|
||||
self.avg_gain = Some(new_ag);
|
||||
self.avg_loss = Some(new_al);
|
||||
if self.avgs_seeded {
|
||||
// Wilder smoothing `(prev·(n-1) + x) / n` with the reciprocal hoisted:
|
||||
// a fused multiply-add then a multiply by `1/n`, no per-tick division.
|
||||
let new_ag = self.avg_gain.mul_add(self.n_minus_1, gain) * self.inv_period;
|
||||
let new_al = self.avg_loss.mul_add(self.n_minus_1, loss) * self.inv_period;
|
||||
self.avg_gain = new_ag;
|
||||
self.avg_loss = new_al;
|
||||
let v = Self::rsi_from_avgs(new_ag, new_al);
|
||||
self.last_value = Some(v);
|
||||
return Some(v);
|
||||
@@ -116,8 +134,9 @@ impl Indicator for Rsi {
|
||||
if self.seed_buf_gains.len() == self.period {
|
||||
let ag = self.seed_buf_gains.iter().sum::<f64>() / self.period as f64;
|
||||
let al = self.seed_buf_losses.iter().sum::<f64>() / self.period as f64;
|
||||
self.avg_gain = Some(ag);
|
||||
self.avg_loss = Some(al);
|
||||
self.avg_gain = ag;
|
||||
self.avg_loss = al;
|
||||
self.avgs_seeded = true;
|
||||
let v = Self::rsi_from_avgs(ag, al);
|
||||
self.last_value = Some(v);
|
||||
return Some(v);
|
||||
@@ -126,11 +145,13 @@ impl Indicator for Rsi {
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.prev_close = None;
|
||||
self.prev_close = 0.0;
|
||||
self.has_prev = false;
|
||||
self.seed_buf_gains.clear();
|
||||
self.seed_buf_losses.clear();
|
||||
self.avg_gain = None;
|
||||
self.avg_loss = None;
|
||||
self.avg_gain = 0.0;
|
||||
self.avg_loss = 0.0;
|
||||
self.avgs_seeded = false;
|
||||
self.last_value = None;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
//! Simple Moving Average.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::traits::Indicator;
|
||||
|
||||
@@ -33,7 +31,14 @@ use crate::traits::Indicator;
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Sma {
|
||||
period: usize,
|
||||
window: VecDeque<f64>,
|
||||
/// Fixed-capacity ring buffer of the last `period` finite inputs. A flat
|
||||
/// `Box<[f64]>` with a manual write cursor beats `VecDeque` on this hot path:
|
||||
/// sequential storage, branchless wraparound, no per-call bookkeeping.
|
||||
buf: Box<[f64]>,
|
||||
/// Index of the next slot to write — also the oldest element once full.
|
||||
head: usize,
|
||||
/// Number of slots filled, saturating at `period`.
|
||||
count: usize,
|
||||
sum: f64,
|
||||
/// Number of finite updates since the running `sum` was last reseeded from
|
||||
/// the live window. Caps accumulated floating-point drift on long streams.
|
||||
@@ -60,7 +65,9 @@ impl Sma {
|
||||
}
|
||||
Ok(Self {
|
||||
period,
|
||||
window: VecDeque::with_capacity(period),
|
||||
buf: vec![0.0; period].into_boxed_slice(),
|
||||
head: 0,
|
||||
count: 0,
|
||||
sum: 0.0,
|
||||
updates_since_recompute: 0,
|
||||
})
|
||||
@@ -73,7 +80,7 @@ impl Sma {
|
||||
|
||||
/// Current value if available.
|
||||
pub fn value(&self) -> Option<f64> {
|
||||
if self.window.len() == self.period {
|
||||
if self.count == self.period {
|
||||
Some(self.sum / self.period as f64)
|
||||
} else {
|
||||
None
|
||||
@@ -89,25 +96,40 @@ impl Indicator for Sma {
|
||||
if !input.is_finite() {
|
||||
return self.value();
|
||||
}
|
||||
if self.window.len() == self.period {
|
||||
// Slide: drop the oldest, then add the new. Each step is a single
|
||||
// f64 add/subtract — O(1) but introduces ~1 ULP of rounding noise.
|
||||
// The periodic reseed below caps the accumulated drift.
|
||||
let old = self.window.pop_front().expect("window non-empty");
|
||||
self.sum -= old;
|
||||
if self.count == self.period {
|
||||
// Window full: overwrite the oldest slot (at `head`). Each step is a
|
||||
// single f64 add/subtract — O(1) but introduces ~1 ULP of rounding
|
||||
// noise. The periodic reseed below caps the accumulated drift.
|
||||
self.sum -= self.buf[self.head];
|
||||
self.buf[self.head] = input;
|
||||
self.sum += input;
|
||||
} else {
|
||||
self.buf[self.head] = input;
|
||||
self.sum += input;
|
||||
self.count += 1;
|
||||
}
|
||||
// Branchless-ish wraparound, cheaper than `% period`.
|
||||
self.head += 1;
|
||||
if self.head == self.period {
|
||||
self.head = 0;
|
||||
}
|
||||
self.window.push_back(input);
|
||||
self.sum += input;
|
||||
self.updates_since_recompute += 1;
|
||||
if self.updates_since_recompute >= RECOMPUTE_EVERY * self.period {
|
||||
self.sum = self.window.iter().copied().sum();
|
||||
// Reseed in chronological order (oldest at `head`) so the running sum
|
||||
// tracks a fresh from-scratch mean to the bit on stable inputs.
|
||||
self.sum = self.buf[self.head..]
|
||||
.iter()
|
||||
.chain(&self.buf[..self.head])
|
||||
.copied()
|
||||
.sum();
|
||||
self.updates_since_recompute = 0;
|
||||
}
|
||||
self.value()
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.window.clear();
|
||||
self.head = 0;
|
||||
self.count = 0;
|
||||
self.sum = 0.0;
|
||||
self.updates_since_recompute = 0;
|
||||
}
|
||||
@@ -117,7 +139,7 @@ impl Indicator for Sma {
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.window.len() == self.period
|
||||
self.count == self.period
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
@@ -130,6 +152,7 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
use std::collections::VecDeque;
|
||||
|
||||
#[test]
|
||||
fn new_rejects_zero_period() {
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
//! Time Series Forecast Oscillator (TSF Oscillator).
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::indicators::tsf::Tsf;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Time Series Forecast Oscillator — the percentage gap between the close and
|
||||
/// the **one-bar-ahead** time-series forecast of the close.
|
||||
///
|
||||
/// ```text
|
||||
/// TSFOsc_t = 100 · (close_t − TSF(close, period)_t) / close_t
|
||||
/// ```
|
||||
///
|
||||
/// where [`Tsf`](crate::Tsf) projects the rolling least-squares line one bar
|
||||
/// past the window (`a + b·period`). It is the close-relative companion to
|
||||
/// [`Cfo`](crate::Cfo), which measures the same percentage gap against the
|
||||
/// regression value at the *current* bar (`a + b·(period − 1)`). Because `TSF`
|
||||
/// advances one bar further than `LinearRegression`, the two differ by exactly
|
||||
/// the slope term `100·b/close`: on a trending series `TSFOsc` reads more
|
||||
/// negative in an uptrend (the forecast has already stepped above price) and
|
||||
/// more positive in a downtrend.
|
||||
///
|
||||
/// Positive readings mean the close sits *above* its forward forecast (price
|
||||
/// has overshot the projected trend); negative readings mean it sits below.
|
||||
/// Wraps the existing `Tsf` so the warmup matches.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Indicator, TsfOscillator};
|
||||
///
|
||||
/// let mut indicator = TsfOscillator::new(14).unwrap();
|
||||
/// let mut last = None;
|
||||
/// for i in 0..40 {
|
||||
/// last = indicator.update(100.0 + f64::from(i));
|
||||
/// }
|
||||
/// assert!(last.is_some());
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TsfOscillator {
|
||||
period: usize,
|
||||
tsf: Tsf,
|
||||
current: Option<f64>,
|
||||
}
|
||||
|
||||
impl TsfOscillator {
|
||||
/// Construct a new TSF oscillator over `period` inputs.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns [`Error::InvalidPeriod`] if `period < 2` — a regression line is
|
||||
/// undefined for fewer than two points.
|
||||
pub fn new(period: usize) -> Result<Self> {
|
||||
if period < 2 {
|
||||
return Err(Error::InvalidPeriod {
|
||||
message: "TSF oscillator needs period >= 2",
|
||||
});
|
||||
}
|
||||
Ok(Self {
|
||||
period,
|
||||
tsf: Tsf::new(period)?,
|
||||
current: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured period.
|
||||
pub const fn period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for TsfOscillator {
|
||||
type Input = f64;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, input: f64) -> Option<f64> {
|
||||
let forecast = self.tsf.update(input)?;
|
||||
// Hold the previous value if the close is zero — the percentage form
|
||||
// is undefined and a return of inf would propagate badly.
|
||||
if input == 0.0 {
|
||||
return self.current;
|
||||
}
|
||||
let value = 100.0 * (input - forecast) / input;
|
||||
self.current = Some(value);
|
||||
Some(value)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.tsf.reset();
|
||||
self.current = None;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.current.is_some()
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"TsfOscillator"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
#[test]
|
||||
fn rejects_short_period() {
|
||||
assert!(matches!(
|
||||
TsfOscillator::new(1),
|
||||
Err(Error::InvalidPeriod { .. })
|
||||
));
|
||||
assert!(matches!(
|
||||
TsfOscillator::new(0),
|
||||
Err(Error::InvalidPeriod { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let osc = TsfOscillator::new(14).unwrap();
|
||||
assert_eq!(osc.period(), 14);
|
||||
assert_eq!(osc.warmup_period(), 14);
|
||||
assert_eq!(osc.name(), "TsfOscillator");
|
||||
assert!(!osc.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reference_value() {
|
||||
// period 3 over [1, 2, 9]: fit y = 0 + 4x, one-bar-ahead TSF at x = 3
|
||||
// is 12. With close = 9, TSFOsc = 100·(9 − 12)/9 = −33.3333…%.
|
||||
let mut osc = TsfOscillator::new(3).unwrap();
|
||||
let out = osc.batch(&[1.0_f64, 2.0, 9.0]);
|
||||
assert!(out[0].is_none());
|
||||
assert!(out[1].is_none());
|
||||
assert_relative_eq!(out[2].unwrap(), -100.0 / 3.0, epsilon = 1e-9);
|
||||
assert!(osc.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn constant_series_yields_zero() {
|
||||
// On a flat series the regression slope is 0, so the one-bar-ahead TSF
|
||||
// equals the constant and close − forecast is exactly 0.
|
||||
let mut osc = TsfOscillator::new(5).unwrap();
|
||||
let out = osc.batch(&[42.0_f64; 30]);
|
||||
for v in out.iter().skip(4).flatten() {
|
||||
assert_relative_eq!(*v, 0.0, epsilon = 1e-12);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn linear_uptrend_reads_negative() {
|
||||
// Unlike CFO (evaluated at the current bar), the forecast steps one bar
|
||||
// ahead, so on a rising line the projection sits above the close and the
|
||||
// oscillator is negative: TSFOsc = −100·slope/close.
|
||||
let mut osc = TsfOscillator::new(5).unwrap();
|
||||
let prices: Vec<f64> = (1..=20).map(|i| f64::from(i) * 2.0).collect();
|
||||
let out = osc.batch(&prices);
|
||||
for v in out.iter().skip(4).flatten() {
|
||||
assert!(*v < 0.0, "uptrend forecast overshoots close, got {v}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn warmup_emits_first_value_at_period() {
|
||||
let mut osc = TsfOscillator::new(3).unwrap();
|
||||
assert_eq!(osc.update(1.0), None);
|
||||
assert_eq!(osc.update(2.0), None);
|
||||
assert!(osc.update(3.0).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let prices: Vec<f64> = (1..=80)
|
||||
.map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 5.0)
|
||||
.collect();
|
||||
let mut a = TsfOscillator::new(14).unwrap();
|
||||
let mut b = TsfOscillator::new(14).unwrap();
|
||||
assert_eq!(
|
||||
a.batch(&prices),
|
||||
prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut osc = TsfOscillator::new(5).unwrap();
|
||||
osc.batch(&(1..=20).map(f64::from).collect::<Vec<_>>());
|
||||
assert!(osc.is_ready());
|
||||
osc.reset();
|
||||
assert!(!osc.is_ready());
|
||||
assert_eq!(osc.update(1.0), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_close_holds_value() {
|
||||
let mut osc = TsfOscillator::new(3).unwrap();
|
||||
osc.batch(&[1.0_f64, 2.0, 3.0]);
|
||||
let before = osc.current;
|
||||
assert_eq!(osc.update(0.0), before);
|
||||
}
|
||||
}
|
||||
@@ -84,32 +84,33 @@ pub use indicators::{
|
||||
FibonacciPivots, FibonacciPivotsOutput, FisherRsi, FisherTransform, FlagPennant, Footprint,
|
||||
FootprintOutput, ForceIndex, FractalChaosBands, FractalChaosBandsOutput, Frama, FundingBasis,
|
||||
FundingRate, FundingRateMean, FundingRateZScore, GainLossRatio, GapSideBySideWhite,
|
||||
GarmanKlassVolatility, Gartley, GatorOscillator, GeneralizedDema, GeometricMa, GoldenPocket,
|
||||
GoldenPocketOutput, GrangerCausality, GravestoneDoji, Hammer, HangingMan, Harami,
|
||||
HeadAndShoulders, HeikinAshi, HeikinAshiOutput, HiLoActivator, HighLowIndex, HighLowRange,
|
||||
HighWave, Hikkake, HikkakeModified, HilbertDominantCycle, HistoricalVolatility, Hma,
|
||||
HoltWinters, HomingPigeon, HtDcPhase, HtPhasor, HtPhasorOutput, HtTrendMode, HurstChannel,
|
||||
HurstChannelOutput, HurstExponent, Ichimoku, IchimokuOutput, IdenticalThreeCrows, InNeck,
|
||||
Inertia, InformationRatio, InitialBalance, InitialBalanceOutput, InstantaneousTrendline,
|
||||
IntradayMomentumIndex, IntradayVolatilityProfile, IntradayVolatilityProfileOutput,
|
||||
InverseFisherTransform, InvertedHammer, Jma, JumpIndicator, KagiBars, KalmanHedgeRatio,
|
||||
KalmanHedgeRatioOutput, Kama, KasePermissionStochastic, KellyCriterion, Keltner, KeltnerOutput,
|
||||
Kicking, KickingByLength, Kst, KstOutput, Kurtosis, Kvo, KylesLambda, LadderBottom,
|
||||
LaguerreRsi, LeadLagCrossCorrelation, LeadLagCrossCorrelationOutput, LinRegAngle,
|
||||
LinRegChannel, LinRegChannelOutput, LinRegIntercept, LinRegSlope, LinearRegression,
|
||||
LiquidationFeatures, LiquidationFeaturesOutput, LogReturn, LongLeggedDoji, LongLine,
|
||||
LongShortRatio, MaEnvelope, MaEnvelopeOutput, MacdExt, MacdFix, MacdIndicator, MacdOutput,
|
||||
Mama, MamaOutput, MarketFacilitationIndex, Marubozu, MassIndex, MatHold, MatchingLow,
|
||||
MaxDrawdown, McClellanOscillator, McClellanSummationIndex, McGinleyDynamic,
|
||||
MedianAbsoluteDeviation, MedianMa, MedianPrice, Mfi, Microprice, MidPoint, MidPrice, MinusDi,
|
||||
MinusDm, Mom, MorningDojiStar, MorningEveningStar, Natr, NewHighsNewLows, Nvi,
|
||||
OIPriceDivergence, OIWeighted, Obv, OmegaRatio, OnNeck, OpenInterestDelta, OpeningMarubozu,
|
||||
OpeningRange, OpeningRangeOutput, OrderBookImbalanceFull, OrderBookImbalanceTop1,
|
||||
OrderBookImbalanceTopN, OrderFlowImbalance, OuHalfLife, OvernightGap, OvernightIntradayReturn,
|
||||
OvernightIntradayReturnOutput, PainIndex, PairSpreadZScore, PairwiseBeta, ParkinsonVolatility,
|
||||
PearsonCorrelation, PercentAboveMa, PercentB, PercentageTrailingStop, Pgo, PiercingDarkCloud,
|
||||
PlusDi, PlusDm, Pmo, PointAndFigureBars, PolarizedFractalEfficiency, Ppo, ProfitFactor, Psar,
|
||||
Pvi, Qqe, QqeOutput, Qstick, QuotedSpread, RSquared, RealizedSpread, RealizedVolatility,
|
||||
GarmanKlassVolatility, Gartley, GatorOscillator, GatorOscillatorOutput, GeneralizedDema,
|
||||
GeometricMa, GoldenPocket, GoldenPocketOutput, GrangerCausality, GravestoneDoji, Hammer,
|
||||
HangingMan, Harami, HeadAndShoulders, HeikinAshi, HeikinAshiOutput, HiLoActivator,
|
||||
HighLowIndex, HighLowRange, HighWave, Hikkake, HikkakeModified, HilbertDominantCycle,
|
||||
HistoricalVolatility, Hma, HoltWinters, HomingPigeon, HtDcPhase, HtPhasor, HtPhasorOutput,
|
||||
HtTrendMode, HurstChannel, HurstChannelOutput, HurstExponent, Ichimoku, IchimokuOutput,
|
||||
IdenticalThreeCrows, InNeck, Inertia, InformationRatio, InitialBalance, InitialBalanceOutput,
|
||||
InstantaneousTrendline, IntradayMomentumIndex, IntradayVolatilityProfile,
|
||||
IntradayVolatilityProfileOutput, InverseFisherTransform, InvertedHammer, Jma, JumpIndicator,
|
||||
KagiBars, KalmanHedgeRatio, KalmanHedgeRatioOutput, Kama, KasePermissionStochastic,
|
||||
KasePermissionStochasticOutput, KellyCriterion, Keltner, KeltnerOutput, Kicking,
|
||||
KickingByLength, Kst, KstOutput, Kurtosis, Kvo, KylesLambda, LadderBottom, LaguerreRsi,
|
||||
LeadLagCrossCorrelation, LeadLagCrossCorrelationOutput, LinRegAngle, LinRegChannel,
|
||||
LinRegChannelOutput, LinRegIntercept, LinRegSlope, LinearRegression, LiquidationFeatures,
|
||||
LiquidationFeaturesOutput, LogReturn, LongLeggedDoji, LongLine, LongShortRatio, MaEnvelope,
|
||||
MaEnvelopeOutput, MacdExt, MacdFix, MacdHistogram, MacdIndicator, MacdOutput, Mama, MamaOutput,
|
||||
MarketFacilitationIndex, Marubozu, MassIndex, MatHold, MatchingLow, MaxDrawdown,
|
||||
McClellanOscillator, McClellanSummationIndex, McGinleyDynamic, MedianAbsoluteDeviation,
|
||||
MedianMa, MedianPrice, Mfi, Microprice, MidPoint, MidPrice, MinusDi, MinusDm, Mom,
|
||||
MorningDojiStar, MorningEveningStar, Natr, NewHighsNewLows, Nvi, OIPriceDivergence, OIWeighted,
|
||||
Obv, OmegaRatio, OnNeck, OpenInterestDelta, OpeningMarubozu, OpeningRange, OpeningRangeOutput,
|
||||
OrderBookImbalanceFull, OrderBookImbalanceTop1, OrderBookImbalanceTopN, OrderFlowImbalance,
|
||||
OuHalfLife, OvernightGap, OvernightIntradayReturn, OvernightIntradayReturnOutput, PainIndex,
|
||||
PairSpreadZScore, PairwiseBeta, ParkinsonVolatility, PearsonCorrelation, PercentAboveMa,
|
||||
PercentB, PercentageTrailingStop, Pgo, PiercingDarkCloud, PlusDi, PlusDm, Pmo,
|
||||
PointAndFigureBars, PolarizedFractalEfficiency, Ppo, PpoHistogram, ProfitFactor, Psar, Pvi,
|
||||
Qqe, QqeOutput, Qstick, QuotedSpread, RSquared, RealizedSpread, RealizedVolatility,
|
||||
RecoveryFactor, RectangleRange, RegimeLabel, RelativeStrengthAB, RelativeStrengthOutput,
|
||||
RenkoBars, RenkoTrailingStop, RickshawMan, RisingThreeMethods, Rmi, Roc, Rocp, Rocr, Rocr100,
|
||||
RogersSatchellVolatility, RollMeasure, RollingCorrelation, RollingCovariance, RollingIqr,
|
||||
@@ -128,11 +129,11 @@ pub use indicators::{
|
||||
ThreeLineStrike, ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth, Thrusting, TickIndex,
|
||||
Tii, TimeOfDayReturnProfile, TimeOfDayReturnProfileOutput, TpoProfile, TpoProfileOutput,
|
||||
TradeImbalance, TrendLabel, TrendStrengthIndex, TreynorRatio, Triangle, Trima, Trin,
|
||||
TripleTopBottom, Trix, TrueRange, Tsf, Tsi, Tsv, TtmSqueeze, TtmSqueezeOutput, TtmTrend,
|
||||
TurnOfMonth, Tweezer, TwoCrows, TypicalPrice, UlcerIndex, UltimateOscillator, UniqueThreeRiver,
|
||||
UpDownVolumeRatio, UpsideGapThreeMethods, UpsideGapTwoCrows, ValueArea, ValueAreaOutput,
|
||||
ValueAtRisk, Variance, VarianceRatio, VerticalHorizontalFilter, Vidya, VoltyStop,
|
||||
VolumeByTimeProfile, VolumeByTimeProfileOutput, VolumeOscillator, VolumePriceTrend,
|
||||
TripleTopBottom, Trix, TrueRange, Tsf, TsfOscillator, Tsi, Tsv, TtmSqueeze, TtmSqueezeOutput,
|
||||
TtmTrend, TurnOfMonth, Tweezer, TwoCrows, TypicalPrice, UlcerIndex, UltimateOscillator,
|
||||
UniqueThreeRiver, UpDownVolumeRatio, UpsideGapThreeMethods, UpsideGapTwoCrows, ValueArea,
|
||||
ValueAreaOutput, ValueAtRisk, Variance, VarianceRatio, VerticalHorizontalFilter, Vidya,
|
||||
VoltyStop, VolumeByTimeProfile, VolumeByTimeProfileOutput, VolumeOscillator, VolumePriceTrend,
|
||||
VolumeProfile, VolumeProfileOutput, Vortex, VortexOutput, Vpin, Vwap, VwapStdDevBands,
|
||||
VwapStdDevBandsOutput, Vwma, Vzo, WavePm, WaveTrend, WaveTrendOutput, Wedge, WeightedClose,
|
||||
WickRatio, WilliamsFractals, WilliamsFractalsOutput, WilliamsR, WinRate, Wma, WoodiePivots,
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ That includes:
|
||||
[Python](https://docs.wickra.org/Quickstart-Python),
|
||||
[Node](https://docs.wickra.org/Quickstart-Node), and
|
||||
[WASM](https://docs.wickra.org/Quickstart-WASM).
|
||||
- A per-indicator deep dive for every one of the **420 indicators** across
|
||||
- A per-indicator deep dive for every one of the **423 indicators** across
|
||||
the sixteen families (Moving Averages, Momentum Oscillators, Trend &
|
||||
Directional, Price Oscillators, Volatility & Bands, Bands & Channels,
|
||||
Trailing Stops, Volume, Price Statistics, Ehlers / Cycle DSP, Pivots &
|
||||
|
||||
Generated
+7
-7
@@ -17,7 +17,7 @@
|
||||
},
|
||||
"../../bindings/node": {
|
||||
"name": "wickra",
|
||||
"version": "0.5.7",
|
||||
"version": "0.5.9",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"devDependencies": {
|
||||
"@napi-rs/cli": "^2.18.0"
|
||||
@@ -26,12 +26,12 @@
|
||||
"node": ">= 18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"wickra-darwin-arm64": "0.5.7",
|
||||
"wickra-darwin-x64": "0.5.7",
|
||||
"wickra-linux-arm64-gnu": "0.5.7",
|
||||
"wickra-linux-x64-gnu": "0.5.7",
|
||||
"wickra-win32-arm64-msvc": "0.5.7",
|
||||
"wickra-win32-x64-msvc": "0.5.7"
|
||||
"wickra-darwin-arm64": "0.5.9",
|
||||
"wickra-darwin-x64": "0.5.9",
|
||||
"wickra-linux-arm64-gnu": "0.5.9",
|
||||
"wickra-linux-x64-gnu": "0.5.9",
|
||||
"wickra-win32-arm64-msvc": "0.5.9",
|
||||
"wickra-win32-x64-msvc": "0.5.9"
|
||||
}
|
||||
},
|
||||
"node_modules/wickra": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "wickra-examples"
|
||||
version = "0.0.0"
|
||||
version.workspace = true
|
||||
publish = false
|
||||
description = "Runnable Rust examples for the Wickra technical-analysis library."
|
||||
authors.workspace = true
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
//! `Ema(20)`. This target now covers every scalar indicator in the catalogue.
|
||||
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
use wickra_core::{AdaptiveCycle, AdaptiveLaguerreFilter, Alma, AnchoredRsi, Apo, Autocorrelation, AverageDrawdown, BatchExt, Beta, BollingerBands, CalmarRatio, CenterOfGravity, Cfo, Cmo, CoefficientOfVariation, ConditionalValueAtRisk, ConnorsRsi, Coppock, CyberneticCycle, Decycler, DecyclerOscillator, Dema, DerivativeOscillator, DetrendedStdDev, DisparityIndex, DoubleBollinger, Dpo, DrawdownDuration, DynamicMomentumIndex, EhlersStochastic, Ehma, ElderImpulse, Ema, EmpiricalModeDecomposition, Expectancy, Fama, FisherRsi, FisherTransform, Frama, GainLossRatio, GeneralizedDema, GeometricMa, HilbertDominantCycle, HistoricalVolatility, Hma, HoltWinters, HtDcPhase, HtPhasor, HtTrendMode, HurstExponent, Indicator, InstantaneousTrendline, InverseFisherTransform, Jma, JumpIndicator, Kama, KellyCriterion, Kst, Kurtosis, LaguerreRsi, LinRegAngle, LinRegChannel, LinRegIntercept, LinRegSlope, LinearRegression, LogReturn, MaEnvelope, MaType, MacdExt, MacdFix, MacdIndicator, Mama, MaxDrawdown, McGinleyDynamic, MedianAbsoluteDeviation, MedianMa, MidPoint, Mom, OmegaRatio, PainIndex, PearsonCorrelation, PercentageTrailingStop, Pmo, PolarizedFractalEfficiency, Ppo, ProfitFactor, Qqe, RSquared, RealizedVolatility, RecoveryFactor, RegimeLabel, RenkoTrailingStop, Rmi, Roc, Rocp, Rocr, Rocr100, RollingIqr, RollingPercentileRank, RollingQuantile, RoofingFilter, Rsi, Rsx, RviVolatility, SharpeRatio, SineWave, SineWeightedMa, Skewness, Sma, Smma, SortinoRatio, SpearmanCorrelation, StandardError, StandardErrorBands, Stc, StdDev, StepTrailingStop, StochRsi, SuperSmoother, Tema, Tii, TrendLabel, TrendStrengthIndex, Trima, Trix, Tsf, Tsi, UlcerIndex, ValueAtRisk, Variance, VerticalHorizontalFilter, Vidya, WavePm, WinRate, Wma, ZScore, ZeroLagMacd, Zlema, T3};
|
||||
use wickra_core::{AdaptiveCycle, AdaptiveLaguerreFilter, Alma, AnchoredRsi, Apo, Autocorrelation, AverageDrawdown, BatchExt, Beta, BollingerBands, CalmarRatio, CenterOfGravity, Cfo, Cmo, CoefficientOfVariation, ConditionalValueAtRisk, ConnorsRsi, Coppock, CyberneticCycle, Decycler, DecyclerOscillator, Dema, DerivativeOscillator, DetrendedStdDev, DisparityIndex, DoubleBollinger, Dpo, DrawdownDuration, DynamicMomentumIndex, EhlersStochastic, Ehma, ElderImpulse, Ema, EmpiricalModeDecomposition, Expectancy, Fama, FisherRsi, FisherTransform, Frama, GainLossRatio, GeneralizedDema, GeometricMa, HilbertDominantCycle, HistoricalVolatility, Hma, HoltWinters, HtDcPhase, HtPhasor, HtTrendMode, HurstExponent, Indicator, InstantaneousTrendline, InverseFisherTransform, Jma, JumpIndicator, Kama, KellyCriterion, Kst, Kurtosis, LaguerreRsi, LinRegAngle, LinRegChannel, LinRegIntercept, LinRegSlope, LinearRegression, LogReturn, MaEnvelope, MaType, MacdExt, MacdFix, MacdHistogram, MacdIndicator, Mama, MaxDrawdown, McGinleyDynamic, MedianAbsoluteDeviation, MedianMa, MidPoint, Mom, OmegaRatio, PainIndex, PearsonCorrelation, PercentageTrailingStop, Pmo, PolarizedFractalEfficiency, Ppo, PpoHistogram, ProfitFactor, Qqe, RSquared, RealizedVolatility, RecoveryFactor, RegimeLabel, RenkoTrailingStop, Rmi, Roc, Rocp, Rocr, Rocr100, RollingIqr, RollingPercentileRank, RollingQuantile, RoofingFilter, Rsi, Rsx, RviVolatility, SharpeRatio, SineWave, SineWeightedMa, Skewness, Sma, Smma, SortinoRatio, SpearmanCorrelation, StandardError, StandardErrorBands, Stc, StdDev, StepTrailingStop, StochRsi, SuperSmoother, Tema, Tii, TrendLabel, TrendStrengthIndex, Trima, Trix, Tsf, TsfOscillator, Tsi, UlcerIndex, ValueAtRisk, Variance, VerticalHorizontalFilter, Vidya, WavePm, WinRate, Wma, ZScore, ZeroLagMacd, Zlema, T3};
|
||||
|
||||
/// Drive a single streaming + batch run through one scalar indicator. Marked
|
||||
/// `#[inline(never)]` so a panic backtrace pin-points the specific indicator.
|
||||
@@ -84,6 +84,9 @@ fuzz_target!(|data: Vec<f64>| {
|
||||
drive(|| Ppo::new(12, 26).unwrap(), &data);
|
||||
drive(|| Apo::new(12, 26).unwrap(), &data);
|
||||
drive(|| Cfo::new(14).unwrap(), &data);
|
||||
drive(|| TsfOscillator::new(14).unwrap(), &data);
|
||||
drive(|| MacdHistogram::new(12, 26, 9).unwrap(), &data);
|
||||
drive(|| PpoHistogram::new(12, 26, 9).unwrap(), &data);
|
||||
drive(|| ElderImpulse::classic(), &data);
|
||||
drive(|| Stc::classic(), &data);
|
||||
drive(|| Coppock::new(14, 11, 10).unwrap(), &data);
|
||||
|
||||
Reference in New Issue
Block a user