feat: init the repo
This commit is contained in:
Generated
+415
@@ -0,0 +1,415 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.89"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
|
||||
|
||||
[[package]]
|
||||
name = "cast"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.56"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "ferro_ta_wasm"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-test",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
"pin-project-lite",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.91"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libm"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||
|
||||
[[package]]
|
||||
name = "minicov"
|
||||
version = "0.3.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4869b6a491569605d66d3952bcdf03df789e5b536e5f0cf7758a7f08a55ae24d"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.50.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"libm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
|
||||
[[package]]
|
||||
name = "oorandom"
|
||||
version = "11.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.149"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
|
||||
dependencies = [
|
||||
"same-file",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.114"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"rustversion",
|
||||
"wasm-bindgen-macro",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-futures"
|
||||
version = "0.4.64"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"futures-util",
|
||||
"js-sys",
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.114"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.114"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.114"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-test"
|
||||
version = "0.3.64"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6311c867385cc7d5602463b31825d454d0837a3aba7cdb5e56d5201792a3f7fe"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"cast",
|
||||
"js-sys",
|
||||
"libm",
|
||||
"minicov",
|
||||
"nu-ansi-term",
|
||||
"num-traits",
|
||||
"oorandom",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"wasm-bindgen-test-macro",
|
||||
"wasm-bindgen-test-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-test-macro"
|
||||
version = "0.3.64"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67008cdde4769831958536b0f11b3bdd0380bde882be17fff9c2f34bb4549abd"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-test-shared"
|
||||
version = "0.2.114"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cfe29135b180b72b04c74aa97b2b4a2ef275161eff9a6c7955ea9eaedc7e1d4e"
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.91"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||
@@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "ferro_ta_wasm"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "WebAssembly bindings for ferro-ta technical analysis indicators"
|
||||
license = "MIT"
|
||||
|
||||
[workspace]
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
wasm-bindgen = "0.2"
|
||||
js-sys = "0.3"
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test = "0.3"
|
||||
|
||||
[profile.release]
|
||||
opt-level = "s" # optimise for size in WASM
|
||||
lto = true
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
# ferro-ta WASM
|
||||
|
||||
WebAssembly bindings for the [ferro-ta](https://github.com/pratikbhadane24/ferro-ta) technical analysis library.
|
||||
|
||||
## Install from npm
|
||||
|
||||
Once published, install the Node.js build from npm:
|
||||
|
||||
```bash
|
||||
npm install ferro-ta-wasm
|
||||
```
|
||||
|
||||
```javascript
|
||||
const { sma, ema, rsi, bbands, atr, obv, macd } = require('ferro-ta-wasm');
|
||||
|
||||
const close = new Float64Array([44.34, 44.09, 44.15, 43.61, 44.33, 44.83, 45.10]);
|
||||
const smaOut = sma(close, 3);
|
||||
console.log('SMA:', Array.from(smaOut));
|
||||
```
|
||||
|
||||
> **Decision**: We chose WebAssembly (wasm-bindgen / wasm-pack) as the second binding because it runs in
|
||||
> browsers *and* Node.js without any native addons, and shares zero unsafe FFI surface with the Python
|
||||
> build. Node.js users get a pure-JS entry point; browser users get the same `.wasm` file.
|
||||
|
||||
## Available Indicators
|
||||
|
||||
| Category | Function | Parameters | Returns |
|
||||
|------------|---------------|----------------------------------------------------|---------|
|
||||
| Overlap | `sma` | `close: Float64Array, timeperiod: number` | `Float64Array` |
|
||||
| Overlap | `ema` | `close: Float64Array, timeperiod: number` | `Float64Array` |
|
||||
| Overlap | `bbands` | `close, timeperiod, nbdevup, nbdevdn` | `Array[upper, middle, lower]` |
|
||||
| Momentum | `rsi` | `close: Float64Array, timeperiod: number` | `Float64Array` |
|
||||
| Momentum | `macd` | `close, fastperiod, slowperiod, signalperiod` | `Array[macd, signal, hist]` |
|
||||
| Momentum | `mom` | `close: Float64Array, timeperiod: number` | `Float64Array` |
|
||||
| Momentum | `stochf` | `high, low, close, fastk_period, fastd_period` | `Array[fastk, fastd]` |
|
||||
| Volatility | `atr` | `high, low, close: Float64Array, timeperiod` | `Float64Array` |
|
||||
| Volume | `obv` | `close: Float64Array, volume: Float64Array` | `Float64Array` |
|
||||
|
||||
### Adding more indicators
|
||||
|
||||
All implementations are self-contained in `src/lib.rs` — no external crate dependency needed.
|
||||
To add a new indicator:
|
||||
|
||||
1. Implement the algorithm in a `#[wasm_bindgen]` function in `src/lib.rs`.
|
||||
2. Add at least two `#[wasm_bindgen_test]` tests covering output length and a known value.
|
||||
3. Update this README table.
|
||||
4. Run `wasm-pack test --node` to verify.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
```bash
|
||||
# Install Rust (if not already present)
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
|
||||
# Install wasm-pack
|
||||
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
|
||||
# OR via cargo:
|
||||
cargo install wasm-pack
|
||||
```
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
cd wasm/
|
||||
wasm-pack build --target nodejs --out-dir pkg
|
||||
```
|
||||
|
||||
This produces a `pkg/` directory containing:
|
||||
- `ferro_ta_wasm.js` — JavaScript glue code
|
||||
- `ferro_ta_wasm_bg.wasm` — compiled WebAssembly binary
|
||||
- `ferro_ta_wasm.d.ts` — TypeScript declarations
|
||||
|
||||
For a browser build:
|
||||
|
||||
```bash
|
||||
wasm-pack build --target web --out-dir pkg-web
|
||||
```
|
||||
|
||||
## Usage (Node.js)
|
||||
|
||||
```javascript
|
||||
const { sma, ema, rsi, bbands, atr, obv, macd } = require('./pkg/ferro_ta_wasm.js');
|
||||
|
||||
const close = new Float64Array([44.34, 44.09, 44.15, 43.61, 44.33, 44.83, 45.10]);
|
||||
|
||||
// Simple Moving Average (period 3)
|
||||
const smaOut = sma(close, 3);
|
||||
console.log('SMA:', Array.from(smaOut)); // [ NaN, NaN, 44.193, ... ]
|
||||
|
||||
// RSI (period 5)
|
||||
const rsiOut = rsi(close, 5);
|
||||
console.log('RSI:', Array.from(rsiOut));
|
||||
|
||||
// Bollinger Bands (period 5, ±2σ) — returns [upper, middle, lower]
|
||||
const [upper, middle, lower] = bbands(close, 5, 2.0, 2.0);
|
||||
console.log('BBANDS upper:', Array.from(upper));
|
||||
|
||||
// MACD (fast=3, slow=5, signal=2) — returns [macd_line, signal_line, histogram]
|
||||
const [macdLine, signalLine, histogram] = macd(close, 3, 5, 2);
|
||||
console.log('MACD:', Array.from(macdLine));
|
||||
console.log('Signal:', Array.from(signalLine));
|
||||
console.log('Histogram:', Array.from(histogram));
|
||||
|
||||
// ATR (period 3)
|
||||
const high = new Float64Array([45.0, 46.0, 47.0, 46.0, 45.0, 44.0, 45.0]);
|
||||
const low = new Float64Array([43.0, 44.0, 45.0, 44.0, 43.0, 42.0, 43.0]);
|
||||
const atrOut = atr(high, low, close, 3);
|
||||
console.log('ATR:', Array.from(atrOut));
|
||||
|
||||
// OBV
|
||||
const volume = new Float64Array([1000, 1200, 900, 1500, 800, 600, 700]);
|
||||
const obvOut = obv(close, volume);
|
||||
console.log('OBV:', Array.from(obvOut));
|
||||
```
|
||||
|
||||
## Usage (Browser)
|
||||
|
||||
```html
|
||||
<script type="module">
|
||||
import init, { sma, macd } from './pkg-web/ferro_ta_wasm.js';
|
||||
await init(); // loads the .wasm binary
|
||||
|
||||
const close = new Float64Array([44.34, 44.09, 44.15, 43.61, 44.33]);
|
||||
const smaOut = sma(close, 3);
|
||||
console.log('SMA:', Array.from(smaOut));
|
||||
|
||||
// MACD
|
||||
const [macdLine, signal, hist] = macd(close, 3, 5, 2);
|
||||
console.log('MACD line:', Array.from(macdLine));
|
||||
</script>
|
||||
```
|
||||
|
||||
## Run Tests
|
||||
|
||||
```bash
|
||||
cd wasm/
|
||||
wasm-pack test --node
|
||||
```
|
||||
|
||||
## CI Artifact
|
||||
|
||||
Every CI run on `main` builds the WASM package and uploads it as a GitHub Actions
|
||||
artifact named `wasm-pkg`. To download the latest pre-built package without building
|
||||
from source:
|
||||
|
||||
1. Go to the [Actions tab](https://github.com/pratikbhadane24/ferro-ta/actions).
|
||||
2. Open the latest successful CI run.
|
||||
3. Download the `wasm-pkg` artifact from the **Artifacts** section.
|
||||
4. Unzip and use `pkg/ferro_ta_wasm.js` directly in your project.
|
||||
|
||||
## Limitations
|
||||
|
||||
- Only 9 indicators are currently exposed (SMA, EMA, BBANDS, RSI, MACD, MOM, STOCHF, ATR, OBV).
|
||||
Additional indicators will be added following the same pattern in `src/lib.rs`.
|
||||
- Large arrays (> 10M bars) may be slow due to JS↔WASM memory copies. For high-throughput
|
||||
use cases prefer the Python (PyO3) binding.
|
||||
- WASM does not support multi-threading natively in browsers (SharedArrayBuffer requires
|
||||
COOP/COEP headers).
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "ferro-ta-wasm",
|
||||
"version": "0.1.0",
|
||||
"description": "WebAssembly bindings for ferro-ta technical analysis indicators",
|
||||
"main": "pkg/ferro_ta_wasm.js",
|
||||
"types": "pkg/ferro_ta_wasm.d.ts",
|
||||
"files": ["pkg"],
|
||||
"scripts": {
|
||||
"build": "wasm-pack build --target nodejs --out-dir pkg",
|
||||
"test": "wasm-pack test --node"
|
||||
},
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/pratikbhadane24/ferro-ta.git",
|
||||
"directory": "wasm"
|
||||
},
|
||||
"homepage": "https://github.com/pratikbhadane24/ferro-ta#readme",
|
||||
"bugs": "https://github.com/pratikbhadane24/ferro-ta/issues",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"devDependencies": {}
|
||||
}
|
||||
+864
@@ -0,0 +1,864 @@
|
||||
/*!
|
||||
# ferro-ta WASM bindings
|
||||
|
||||
WebAssembly bindings for the ferro-ta technical analysis library.
|
||||
|
||||
All functions accept `Float64Array` inputs and return `Float64Array` (or a
|
||||
`js_sys::Array` of `Float64Array` for multi-output indicators such as `BBANDS`
|
||||
and `MACD`).
|
||||
|
||||
## Overlap Studies
|
||||
- [`sma`] — Simple Moving Average
|
||||
- [`ema`] — Exponential Moving Average
|
||||
- [`bbands`] — Bollinger Bands (returns `[upper, middle, lower]`)
|
||||
|
||||
## Momentum Indicators
|
||||
- [`rsi`] — Relative Strength Index (Wilder smoothing)
|
||||
- [`macd`] — Moving Average Convergence/Divergence (returns `[macd, signal, hist]`)
|
||||
- [`mom`] — Momentum (close[i] - close[i-period])
|
||||
- [`stochf`] — Fast Stochastic (returns `[fastk, fastd]`)
|
||||
|
||||
## Volatility Indicators
|
||||
- [`atr`] — Average True Range (Wilder smoothing)
|
||||
|
||||
## Volume Indicators
|
||||
- [`obv`] — On-Balance Volume
|
||||
*/
|
||||
|
||||
use js_sys::{Array, Float64Array};
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Copy a `Float64Array` into a `Vec<f64>`.
|
||||
fn to_vec(arr: &Float64Array) -> Vec<f64> {
|
||||
let n = arr.length() as usize;
|
||||
let mut v = vec![0.0f64; n];
|
||||
arr.copy_to(&mut v);
|
||||
v
|
||||
}
|
||||
|
||||
/// Create a `Float64Array` from a `Vec<f64>`.
|
||||
fn from_vec(v: Vec<f64>) -> Float64Array {
|
||||
// Safety: Float64Array::view requires the backing Vec to stay alive for the
|
||||
// duration of the copy. We immediately copy via `Float64Array::from` so
|
||||
// there is no aliasing.
|
||||
let arr = Float64Array::new_with_length(v.len() as u32);
|
||||
arr.copy_from(&v);
|
||||
arr
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SMA — Simple Moving Average
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Simple Moving Average.
|
||||
///
|
||||
/// # Arguments
|
||||
/// - `close` – `Float64Array` of close prices.
|
||||
/// - `timeperiod` – look-back window (default 30, minimum 1).
|
||||
///
|
||||
/// # Returns
|
||||
/// `Float64Array` with the first `timeperiod - 1` values set to `NaN`.
|
||||
#[wasm_bindgen]
|
||||
pub fn sma(close: &Float64Array, timeperiod: usize) -> Float64Array {
|
||||
let prices = to_vec(close);
|
||||
let n = prices.len();
|
||||
let mut result = vec![f64::NAN; n];
|
||||
|
||||
if timeperiod == 0 || n < timeperiod {
|
||||
return from_vec(result);
|
||||
}
|
||||
|
||||
// Seed: sum of first window
|
||||
let mut window_sum: f64 = prices[..timeperiod].iter().sum();
|
||||
result[timeperiod - 1] = window_sum / timeperiod as f64;
|
||||
|
||||
for i in timeperiod..n {
|
||||
window_sum += prices[i] - prices[i - timeperiod];
|
||||
result[i] = window_sum / timeperiod as f64;
|
||||
}
|
||||
|
||||
from_vec(result)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// EMA — Exponential Moving Average
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Exponential Moving Average (SMA-seeded).
|
||||
///
|
||||
/// # Arguments
|
||||
/// - `close` – `Float64Array` of close prices.
|
||||
/// - `timeperiod` – look-back period (default 30, minimum 1).
|
||||
///
|
||||
/// # Returns
|
||||
/// `Float64Array` with the first `timeperiod - 1` values set to `NaN`.
|
||||
#[wasm_bindgen]
|
||||
pub fn ema(close: &Float64Array, timeperiod: usize) -> Float64Array {
|
||||
let prices = to_vec(close);
|
||||
let n = prices.len();
|
||||
let mut result = vec![f64::NAN; n];
|
||||
|
||||
if timeperiod == 0 || n < timeperiod {
|
||||
return from_vec(result);
|
||||
}
|
||||
|
||||
let k = 2.0 / (timeperiod as f64 + 1.0);
|
||||
|
||||
// Seed with SMA of first window
|
||||
let seed: f64 = prices[..timeperiod].iter().sum::<f64>() / timeperiod as f64;
|
||||
result[timeperiod - 1] = seed;
|
||||
let mut prev = seed;
|
||||
|
||||
for i in timeperiod..n {
|
||||
let val = prices[i] * k + prev * (1.0 - k);
|
||||
result[i] = val;
|
||||
prev = val;
|
||||
}
|
||||
|
||||
from_vec(result)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// BBANDS — Bollinger Bands
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Bollinger Bands (SMA ± k × rolling standard deviation).
|
||||
///
|
||||
/// # Arguments
|
||||
/// - `close` – `Float64Array` of close prices.
|
||||
/// - `timeperiod` – look-back window (default 5, minimum 1).
|
||||
/// - `nbdevup` – multiplier for the upper band (default 2.0).
|
||||
/// - `nbdevdn` – multiplier for the lower band (default 2.0).
|
||||
///
|
||||
/// # Returns
|
||||
/// A `js_sys::Array` containing three `Float64Array` elements:
|
||||
/// `[upperband, middleband, lowerband]`.
|
||||
#[wasm_bindgen]
|
||||
pub fn bbands(
|
||||
close: &Float64Array,
|
||||
timeperiod: usize,
|
||||
nbdevup: f64,
|
||||
nbdevdn: f64,
|
||||
) -> Array {
|
||||
let prices = to_vec(close);
|
||||
let n = prices.len();
|
||||
let mut upper = vec![f64::NAN; n];
|
||||
let mut middle = vec![f64::NAN; n];
|
||||
let mut lower = vec![f64::NAN; n];
|
||||
|
||||
if timeperiod == 0 || n < timeperiod {
|
||||
let out = Array::new();
|
||||
out.push(&from_vec(upper));
|
||||
out.push(&from_vec(middle));
|
||||
out.push(&from_vec(lower));
|
||||
return out;
|
||||
}
|
||||
|
||||
for i in (timeperiod - 1)..n {
|
||||
let window = &prices[(i + 1 - timeperiod)..=i];
|
||||
let mean = window.iter().sum::<f64>() / timeperiod as f64;
|
||||
let variance = window.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / timeperiod as f64;
|
||||
let stddev = variance.sqrt();
|
||||
middle[i] = mean;
|
||||
upper[i] = mean + nbdevup * stddev;
|
||||
lower[i] = mean - nbdevdn * stddev;
|
||||
}
|
||||
|
||||
let out = Array::new();
|
||||
out.push(&from_vec(upper));
|
||||
out.push(&from_vec(middle));
|
||||
out.push(&from_vec(lower));
|
||||
out
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// RSI — Relative Strength Index (Wilder smoothing, TA-Lib compatible)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Relative Strength Index (Wilder smoothing).
|
||||
///
|
||||
/// # Arguments
|
||||
/// - `close` – `Float64Array` of close prices.
|
||||
/// - `timeperiod` – look-back period (default 14, minimum 1).
|
||||
///
|
||||
/// # Returns
|
||||
/// `Float64Array` — values in `[0, 100]`; first `timeperiod` values are `NaN`.
|
||||
#[wasm_bindgen]
|
||||
pub fn rsi(close: &Float64Array, timeperiod: usize) -> Float64Array {
|
||||
let prices = to_vec(close);
|
||||
let n = prices.len();
|
||||
let mut result = vec![f64::NAN; n];
|
||||
|
||||
if timeperiod == 0 || n <= timeperiod {
|
||||
return from_vec(result);
|
||||
}
|
||||
|
||||
// Compute gains and losses
|
||||
let diffs: Vec<f64> = prices.windows(2).map(|w| w[1] - w[0]).collect();
|
||||
|
||||
// Seed average gain / loss over first `timeperiod` bars
|
||||
let mut avg_gain: f64 = diffs[..timeperiod]
|
||||
.iter()
|
||||
.map(|&d| if d > 0.0 { d } else { 0.0 })
|
||||
.sum::<f64>()
|
||||
/ timeperiod as f64;
|
||||
let mut avg_loss: f64 = diffs[..timeperiod]
|
||||
.iter()
|
||||
.map(|&d| if d < 0.0 { -d } else { 0.0 })
|
||||
.sum::<f64>()
|
||||
/ timeperiod as f64;
|
||||
|
||||
// First RSI value at index `timeperiod`
|
||||
let rs = if avg_loss == 0.0 { f64::INFINITY } else { avg_gain / avg_loss };
|
||||
result[timeperiod] = 100.0 - 100.0 / (1.0 + rs);
|
||||
|
||||
// Wilder smoothing for remaining values
|
||||
for i in (timeperiod + 1)..n {
|
||||
let diff = diffs[i - 1];
|
||||
let gain = if diff > 0.0 { diff } else { 0.0 };
|
||||
let loss = if diff < 0.0 { -diff } else { 0.0 };
|
||||
avg_gain = (avg_gain * (timeperiod as f64 - 1.0) + gain) / timeperiod as f64;
|
||||
avg_loss = (avg_loss * (timeperiod as f64 - 1.0) + loss) / timeperiod as f64;
|
||||
let rs = if avg_loss == 0.0 { f64::INFINITY } else { avg_gain / avg_loss };
|
||||
result[i] = 100.0 - 100.0 / (1.0 + rs);
|
||||
}
|
||||
|
||||
from_vec(result)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ATR — Average True Range (Wilder smoothing)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Average True Range (Wilder smoothing, TA-Lib compatible).
|
||||
///
|
||||
/// # Arguments
|
||||
/// - `high` – `Float64Array` of high prices.
|
||||
/// - `low` – `Float64Array` of low prices.
|
||||
/// - `close` – `Float64Array` of close prices.
|
||||
/// - `timeperiod` – look-back period (default 14, minimum 1).
|
||||
///
|
||||
/// # Returns
|
||||
/// `Float64Array`; first `timeperiod` values are `NaN`.
|
||||
#[wasm_bindgen]
|
||||
pub fn atr(
|
||||
high: &Float64Array,
|
||||
low: &Float64Array,
|
||||
close: &Float64Array,
|
||||
timeperiod: usize,
|
||||
) -> Float64Array {
|
||||
let h = to_vec(high);
|
||||
let l = to_vec(low);
|
||||
let c = to_vec(close);
|
||||
let n = h.len();
|
||||
let mut result = vec![f64::NAN; n];
|
||||
|
||||
if timeperiod == 0 || n <= timeperiod {
|
||||
return from_vec(result);
|
||||
}
|
||||
if l.len() != n || c.len() != n {
|
||||
return from_vec(result);
|
||||
}
|
||||
|
||||
// True Range for each bar
|
||||
let mut tr = vec![0.0f64; n];
|
||||
tr[0] = h[0] - l[0]; // first bar: no previous close
|
||||
for i in 1..n {
|
||||
let hl = h[i] - l[i];
|
||||
let hpc = (h[i] - c[i - 1]).abs();
|
||||
let lpc = (l[i] - c[i - 1]).abs();
|
||||
tr[i] = hl.max(hpc).max(lpc);
|
||||
}
|
||||
|
||||
// Seed: SMA of first `timeperiod` true ranges
|
||||
let seed: f64 = tr[1..=timeperiod].iter().sum::<f64>() / timeperiod as f64;
|
||||
result[timeperiod] = seed;
|
||||
let mut prev = seed;
|
||||
|
||||
for i in (timeperiod + 1)..n {
|
||||
let val = (prev * (timeperiod as f64 - 1.0) + tr[i]) / timeperiod as f64;
|
||||
result[i] = val;
|
||||
prev = val;
|
||||
}
|
||||
|
||||
from_vec(result)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// OBV — On-Balance Volume
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// On-Balance Volume.
|
||||
///
|
||||
/// # Arguments
|
||||
/// - `close` – `Float64Array` of close prices.
|
||||
/// - `volume` – `Float64Array` of volume values.
|
||||
///
|
||||
/// # Returns
|
||||
/// `Float64Array` — cumulative OBV.
|
||||
#[wasm_bindgen]
|
||||
pub fn obv(close: &Float64Array, volume: &Float64Array) -> Float64Array {
|
||||
let c = to_vec(close);
|
||||
let v = to_vec(volume);
|
||||
let n = c.len();
|
||||
let mut result = vec![0.0f64; n];
|
||||
|
||||
if n == 0 || v.len() != n {
|
||||
return from_vec(result);
|
||||
}
|
||||
|
||||
result[0] = v[0];
|
||||
for i in 1..n {
|
||||
if c[i] > c[i - 1] {
|
||||
result[i] = result[i - 1] + v[i];
|
||||
} else if c[i] < c[i - 1] {
|
||||
result[i] = result[i - 1] - v[i];
|
||||
} else {
|
||||
result[i] = result[i - 1];
|
||||
}
|
||||
}
|
||||
|
||||
from_vec(result)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// MOM — Momentum
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Momentum — difference between current close and close *timeperiod* bars ago.
|
||||
///
|
||||
/// # Arguments
|
||||
/// - `close` – `Float64Array` of close prices.
|
||||
/// - `timeperiod` – look-back window (default 10, minimum 1).
|
||||
///
|
||||
/// # Returns
|
||||
/// `Float64Array`; first `timeperiod` values are `NaN`.
|
||||
#[wasm_bindgen]
|
||||
pub fn mom(close: &Float64Array, timeperiod: usize) -> Float64Array {
|
||||
let prices = to_vec(close);
|
||||
let n = prices.len();
|
||||
let mut result = vec![f64::NAN; n];
|
||||
|
||||
if timeperiod == 0 || n <= timeperiod {
|
||||
return from_vec(result);
|
||||
}
|
||||
|
||||
for i in timeperiod..n {
|
||||
result[i] = prices[i] - prices[i - timeperiod];
|
||||
}
|
||||
|
||||
from_vec(result)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// STOCHF — Fast Stochastic Oscillator
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Fast Stochastic Oscillator.
|
||||
///
|
||||
/// # Arguments
|
||||
/// - `high` – `Float64Array` of high prices.
|
||||
/// - `low` – `Float64Array` of low prices.
|
||||
/// - `close` – `Float64Array` of close prices.
|
||||
/// - `fastk_period` – fast-%K look-back window (default 5, minimum 1).
|
||||
/// - `fastd_period` – fast-%D SMA smoothing period (default 3, minimum 1).
|
||||
///
|
||||
/// # Returns
|
||||
/// A `js_sys::Array` containing two `Float64Array` elements: `[fastk, fastd]`.
|
||||
#[wasm_bindgen]
|
||||
pub fn stochf(
|
||||
high: &Float64Array,
|
||||
low: &Float64Array,
|
||||
close: &Float64Array,
|
||||
fastk_period: usize,
|
||||
fastd_period: usize,
|
||||
) -> Array {
|
||||
let h = to_vec(high);
|
||||
let l = to_vec(low);
|
||||
let c = to_vec(close);
|
||||
let n = c.len();
|
||||
|
||||
let nan_out = || {
|
||||
let out = Array::new();
|
||||
out.push(&from_vec(vec![f64::NAN; n]));
|
||||
out.push(&from_vec(vec![f64::NAN; n]));
|
||||
out
|
||||
};
|
||||
|
||||
if fastk_period == 0 || fastd_period == 0 || n < fastk_period {
|
||||
return nan_out();
|
||||
}
|
||||
if h.len() != n || l.len() != n {
|
||||
return nan_out();
|
||||
}
|
||||
|
||||
// Fast %K: (close - lowest_low) / (highest_high - lowest_low) * 100
|
||||
let mut fastk = vec![f64::NAN; n];
|
||||
for i in (fastk_period - 1)..n {
|
||||
let low_min = l[(i + 1 - fastk_period)..=i]
|
||||
.iter()
|
||||
.cloned()
|
||||
.fold(f64::INFINITY, f64::min);
|
||||
let high_max = h[(i + 1 - fastk_period)..=i]
|
||||
.iter()
|
||||
.cloned()
|
||||
.fold(f64::NEG_INFINITY, f64::max);
|
||||
let range = high_max - low_min;
|
||||
fastk[i] = if range > 0.0 {
|
||||
(c[i] - low_min) / range * 100.0
|
||||
} else {
|
||||
50.0 // all bars at same price — neutral
|
||||
};
|
||||
}
|
||||
|
||||
// Fast %D: SMA(fastd_period) of fast %K
|
||||
let mut fastd = vec![f64::NAN; n];
|
||||
let k_start = fastk_period - 1;
|
||||
if n >= k_start + fastd_period {
|
||||
for i in (k_start + fastd_period - 1)..n {
|
||||
let window = &fastk[(i + 1 - fastd_period)..=i];
|
||||
if window.iter().all(|x| x.is_finite()) {
|
||||
fastd[i] = window.iter().sum::<f64>() / fastd_period as f64;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let out = Array::new();
|
||||
out.push(&from_vec(fastk));
|
||||
out.push(&from_vec(fastd));
|
||||
out
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// MACD — Moving Average Convergence/Divergence
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Moving Average Convergence/Divergence.
|
||||
///
|
||||
/// # Arguments
|
||||
/// - `close` – `Float64Array` of close prices.
|
||||
/// - `fastperiod` – fast EMA period (default 12).
|
||||
/// - `slowperiod` – slow EMA period (default 26).
|
||||
/// - `signalperiod` – signal EMA period (default 9).
|
||||
///
|
||||
/// # Returns
|
||||
/// A `js_sys::Array` containing three `Float64Array` elements:
|
||||
/// `[macd_line, signal_line, histogram]`.
|
||||
#[wasm_bindgen]
|
||||
pub fn macd(
|
||||
close: &Float64Array,
|
||||
fastperiod: usize,
|
||||
slowperiod: usize,
|
||||
signalperiod: usize,
|
||||
) -> Array {
|
||||
let prices = to_vec(close);
|
||||
let n = prices.len();
|
||||
let nan_result = || {
|
||||
let out = Array::new();
|
||||
out.push(&from_vec(vec![f64::NAN; n]));
|
||||
out.push(&from_vec(vec![f64::NAN; n]));
|
||||
out.push(&from_vec(vec![f64::NAN; n]));
|
||||
out
|
||||
};
|
||||
|
||||
if fastperiod == 0 || slowperiod == 0 || signalperiod == 0 || fastperiod >= slowperiod {
|
||||
return nan_result();
|
||||
}
|
||||
if n < slowperiod {
|
||||
return nan_result();
|
||||
}
|
||||
|
||||
// Helper: SMA-seeded EMA
|
||||
let ema_vec = |data: &[f64], period: usize| -> Vec<f64> {
|
||||
let len = data.len();
|
||||
let mut result = vec![f64::NAN; len];
|
||||
if period == 0 || len < period {
|
||||
return result;
|
||||
}
|
||||
let k = 2.0 / (period as f64 + 1.0);
|
||||
let seed: f64 = data[..period].iter().sum::<f64>() / period as f64;
|
||||
result[period - 1] = seed;
|
||||
for i in period..len {
|
||||
result[i] = data[i] * k + result[i - 1] * (1.0 - k);
|
||||
}
|
||||
result
|
||||
};
|
||||
|
||||
let fast_ema = ema_vec(&prices, fastperiod);
|
||||
let slow_ema = ema_vec(&prices, slowperiod);
|
||||
|
||||
// MACD line = fast EMA − slow EMA (valid from index slowperiod - 1)
|
||||
let mut macd_line = vec![f64::NAN; n];
|
||||
for i in (slowperiod - 1)..n {
|
||||
if fast_ema[i].is_finite() && slow_ema[i].is_finite() {
|
||||
macd_line[i] = fast_ema[i] - slow_ema[i];
|
||||
}
|
||||
}
|
||||
|
||||
// Signal line = EMA(signalperiod) of macd_line, seeded at index slowperiod - 1
|
||||
let macd_start = slowperiod - 1;
|
||||
let mut signal_line = vec![f64::NAN; n];
|
||||
let signal_seed_end = macd_start + signalperiod;
|
||||
if signal_seed_end > n {
|
||||
let out = Array::new();
|
||||
out.push(&from_vec(macd_line.clone()));
|
||||
out.push(&from_vec(signal_line));
|
||||
out.push(&from_vec(vec![f64::NAN; n]));
|
||||
return out;
|
||||
}
|
||||
|
||||
// Seed: SMA of first signalperiod MACD values
|
||||
let seed: f64 = macd_line[macd_start..signal_seed_end]
|
||||
.iter()
|
||||
.sum::<f64>()
|
||||
/ signalperiod as f64;
|
||||
signal_line[signal_seed_end - 1] = seed;
|
||||
let k = 2.0 / (signalperiod as f64 + 1.0);
|
||||
for i in signal_seed_end..n {
|
||||
if macd_line[i].is_finite() {
|
||||
signal_line[i] = macd_line[i] * k + signal_line[i - 1] * (1.0 - k);
|
||||
}
|
||||
}
|
||||
|
||||
// Histogram = MACD − signal
|
||||
let mut histogram = vec![f64::NAN; n];
|
||||
for i in (signal_seed_end - 1)..n {
|
||||
if macd_line[i].is_finite() && signal_line[i].is_finite() {
|
||||
histogram[i] = macd_line[i] - signal_line[i];
|
||||
}
|
||||
}
|
||||
|
||||
let out = Array::new();
|
||||
out.push(&from_vec(macd_line));
|
||||
out.push(&from_vec(signal_line));
|
||||
out.push(&from_vec(histogram));
|
||||
out
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// WASM tests (run with `wasm-pack test --node`)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use wasm_bindgen_test::wasm_bindgen_test;
|
||||
|
||||
fn make_arr(v: &[f64]) -> Float64Array {
|
||||
let arr = Float64Array::new_with_length(v.len() as u32);
|
||||
arr.copy_from(v);
|
||||
arr
|
||||
}
|
||||
|
||||
fn get_finite(arr: &Float64Array) -> Vec<f64> {
|
||||
let mut v = vec![0.0f64; arr.length() as usize];
|
||||
arr.copy_to(&mut v);
|
||||
v.into_iter().filter(|x| x.is_finite()).collect()
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// SMA tests
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_sma_output_length() {
|
||||
let close = make_arr(&[1.0, 2.0, 3.0, 4.0, 5.0]);
|
||||
let out = sma(&close, 3);
|
||||
assert_eq!(out.length(), 5);
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_sma_known_value() {
|
||||
// SMA(3) of [1,2,3,4,5]: first valid at index 2 = (1+2+3)/3 = 2.0
|
||||
let close = make_arr(&[1.0, 2.0, 3.0, 4.0, 5.0]);
|
||||
let out = sma(&close, 3);
|
||||
let vals: Vec<f64> = {
|
||||
let mut v = vec![0.0f64; 5];
|
||||
out.copy_to(&mut v);
|
||||
v
|
||||
};
|
||||
assert!(vals[0].is_nan());
|
||||
assert!(vals[1].is_nan());
|
||||
assert!((vals[2] - 2.0).abs() < 1e-10);
|
||||
assert!((vals[3] - 3.0).abs() < 1e-10);
|
||||
assert!((vals[4] - 4.0).abs() < 1e-10);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// EMA tests
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_ema_output_length() {
|
||||
let close = make_arr(&[1.0, 2.0, 3.0, 4.0, 5.0]);
|
||||
let out = ema(&close, 3);
|
||||
assert_eq!(out.length(), 5);
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_ema_seed_equals_sma() {
|
||||
// Seed of EMA(3) at index 2 should equal SMA(3) = 2.0
|
||||
let close = make_arr(&[1.0, 2.0, 3.0, 4.0, 5.0]);
|
||||
let out = ema(&close, 3);
|
||||
let mut vals = vec![0.0f64; 5];
|
||||
out.copy_to(&mut vals);
|
||||
assert!((vals[2] - 2.0).abs() < 1e-10);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// BBANDS tests
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_bbands_returns_three_arrays() {
|
||||
let close = make_arr(&[1.0, 2.0, 3.0, 4.0, 5.0]);
|
||||
let out = bbands(&close, 3, 2.0, 2.0);
|
||||
assert_eq!(out.length(), 3);
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_bbands_middle_equals_sma() {
|
||||
let data = [44.34, 44.09, 44.15, 43.61, 44.33, 44.83, 45.10];
|
||||
let close = make_arr(&data);
|
||||
let bands = bbands(&close, 3, 2.0, 2.0);
|
||||
|
||||
// Middle band should equal SMA(3)
|
||||
let middle = Float64Array::from(bands.get(1));
|
||||
let sma_out = sma(&close, 3);
|
||||
|
||||
let mut m = vec![0.0f64; 7];
|
||||
middle.copy_to(&mut m);
|
||||
let mut s = vec![0.0f64; 7];
|
||||
sma_out.copy_to(&mut s);
|
||||
|
||||
for i in 2..7 {
|
||||
assert!((m[i] - s[i]).abs() < 1e-10, "middle[{i}] != sma[{i}]");
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_bbands_upper_greater_than_lower() {
|
||||
let data = [44.34, 44.09, 44.15, 43.61, 44.33, 44.83, 45.10];
|
||||
let close = make_arr(&data);
|
||||
let bands = bbands(&close, 3, 2.0, 2.0);
|
||||
let upper = Float64Array::from(bands.get(0));
|
||||
let lower = Float64Array::from(bands.get(2));
|
||||
let mut u = vec![0.0f64; 7];
|
||||
let mut l = vec![0.0f64; 7];
|
||||
upper.copy_to(&mut u);
|
||||
lower.copy_to(&mut l);
|
||||
for i in 2..7 {
|
||||
assert!(u[i] >= l[i], "upper[{i}] < lower[{i}]");
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// RSI tests
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_rsi_output_length() {
|
||||
let close = make_arr(&[
|
||||
44.34, 44.09, 44.15, 43.61, 44.33, 44.83, 45.10, 45.15,
|
||||
43.61, 44.33, 44.83, 45.10, 45.15, 43.61, 44.33,
|
||||
]);
|
||||
let out = rsi(&close, 14);
|
||||
assert_eq!(out.length(), 15);
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_rsi_range_0_to_100() {
|
||||
let close = make_arr(&[
|
||||
44.34, 44.09, 44.15, 43.61, 44.33, 44.83, 45.10, 45.15,
|
||||
43.61, 44.33, 44.83, 45.10, 45.15, 43.61, 44.33,
|
||||
]);
|
||||
let out = rsi(&close, 5);
|
||||
let finite = get_finite(&out);
|
||||
for v in finite {
|
||||
assert!(v >= 0.0 && v <= 100.0, "RSI out of range: {v}");
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// ATR tests
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_atr_output_length() {
|
||||
let high = make_arr(&[45.0, 46.0, 47.0, 46.0, 45.0, 44.0, 45.0]);
|
||||
let low = make_arr(&[43.0, 44.0, 45.0, 44.0, 43.0, 42.0, 43.0]);
|
||||
let close = make_arr(&[44.0, 45.0, 46.0, 45.0, 44.0, 43.0, 44.0]);
|
||||
let out = atr(&high, &low, &close, 3);
|
||||
assert_eq!(out.length(), 7);
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_atr_all_positive() {
|
||||
let high = make_arr(&[45.0, 46.0, 47.0, 46.0, 45.0, 44.0, 45.0]);
|
||||
let low = make_arr(&[43.0, 44.0, 45.0, 44.0, 43.0, 42.0, 43.0]);
|
||||
let close = make_arr(&[44.0, 45.0, 46.0, 45.0, 44.0, 43.0, 44.0]);
|
||||
let out = atr(&high, &low, &close, 3);
|
||||
let finite = get_finite(&out);
|
||||
assert!(!finite.is_empty());
|
||||
for v in finite {
|
||||
assert!(v > 0.0, "ATR should be positive, got {v}");
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// OBV tests
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_obv_output_length() {
|
||||
let close = make_arr(&[10.0, 11.0, 10.0, 12.0, 11.0]);
|
||||
let volume = make_arr(&[100.0, 200.0, 150.0, 300.0, 250.0]);
|
||||
let out = obv(&close, &volume);
|
||||
assert_eq!(out.length(), 5);
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_obv_known_values() {
|
||||
// close: 10 → 11 (up, +200) → 10 (dn, -150) → 12 (up, +300) → 11 (dn, -250)
|
||||
// OBV: 100, 300, 150, 450, 200
|
||||
let close = make_arr(&[10.0, 11.0, 10.0, 12.0, 11.0]);
|
||||
let volume = make_arr(&[100.0, 200.0, 150.0, 300.0, 250.0]);
|
||||
let out = obv(&close, &volume);
|
||||
let mut vals = vec![0.0f64; 5];
|
||||
out.copy_to(&mut vals);
|
||||
assert!((vals[0] - 100.0).abs() < 1e-10);
|
||||
assert!((vals[1] - 300.0).abs() < 1e-10);
|
||||
assert!((vals[2] - 150.0).abs() < 1e-10);
|
||||
assert!((vals[3] - 450.0).abs() < 1e-10);
|
||||
assert!((vals[4] - 200.0).abs() < 1e-10);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// MACD tests
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_macd_returns_three_arrays() {
|
||||
let data = [
|
||||
44.34, 44.09, 44.15, 43.61, 44.33, 44.83, 45.10, 45.15,
|
||||
43.61, 44.33, 44.83, 45.10, 45.15, 43.61, 44.33,
|
||||
44.34, 44.09, 44.15, 43.61, 44.33, 44.83, 45.10, 45.15,
|
||||
43.61, 44.33, 44.83, 45.10, 45.15, 43.61, 44.33,
|
||||
];
|
||||
let close = make_arr(&data);
|
||||
let out = macd(&close, 3, 5, 2);
|
||||
assert_eq!(out.length(), 3);
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_macd_output_length() {
|
||||
let data: Vec<f64> = (1..=30).map(|x| x as f64 * 1.0).collect();
|
||||
let close = make_arr(&data);
|
||||
let out = macd(&close, 3, 5, 2);
|
||||
let macd_line = Float64Array::from(out.get(0));
|
||||
assert_eq!(macd_line.length(), 30);
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_macd_finite_values_after_warmup() {
|
||||
// With fastperiod=3, slowperiod=5, signalperiod=2:
|
||||
// MACD line valid from index 4; signal from index 5.
|
||||
let data: Vec<f64> = (1..=20).map(|x| x as f64).collect();
|
||||
let close = make_arr(&data);
|
||||
let out = macd(&close, 3, 5, 2);
|
||||
let signal = Float64Array::from(out.get(1));
|
||||
let finite = get_finite(&signal);
|
||||
assert!(!finite.is_empty(), "signal should have finite values");
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_macd_histogram_is_macd_minus_signal() {
|
||||
let data: Vec<f64> = (1..=20).map(|x| x as f64).collect();
|
||||
let close = make_arr(&data);
|
||||
let out = macd(&close, 3, 5, 2);
|
||||
let macd_arr = Float64Array::from(out.get(0));
|
||||
let sig_arr = Float64Array::from(out.get(1));
|
||||
let hist_arr = Float64Array::from(out.get(2));
|
||||
|
||||
let n = macd_arr.length() as usize;
|
||||
let mut m = vec![0.0f64; n];
|
||||
let mut s = vec![0.0f64; n];
|
||||
let mut h = vec![0.0f64; n];
|
||||
macd_arr.copy_to(&mut m);
|
||||
sig_arr.copy_to(&mut s);
|
||||
hist_arr.copy_to(&mut h);
|
||||
|
||||
for i in 0..n {
|
||||
if m[i].is_finite() && s[i].is_finite() {
|
||||
assert!((h[i] - (m[i] - s[i])).abs() < 1e-10,
|
||||
"histogram[{i}] != macd[{i}] - signal[{i}]");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// MOM tests
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_mom_output_length() {
|
||||
let close = make_arr(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0]);
|
||||
let out = mom(&close, 3);
|
||||
assert_eq!(out.length(), 7);
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_mom_known_values() {
|
||||
// MOM(2) of [1,2,3,4,5]: NaN, NaN, 2.0, 2.0, 2.0
|
||||
let close = make_arr(&[1.0, 2.0, 3.0, 4.0, 5.0]);
|
||||
let out = mom(&close, 2);
|
||||
let mut vals = vec![0.0f64; 5];
|
||||
out.copy_to(&mut vals);
|
||||
assert!(vals[0].is_nan());
|
||||
assert!(vals[1].is_nan());
|
||||
assert!((vals[2] - 2.0).abs() < 1e-10, "MOM[2] should be 2.0");
|
||||
assert!((vals[3] - 2.0).abs() < 1e-10, "MOM[3] should be 2.0");
|
||||
assert!((vals[4] - 2.0).abs() < 1e-10, "MOM[4] should be 2.0");
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// STOCHF tests
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_stochf_returns_two_arrays() {
|
||||
let h = make_arr(&[10.0, 11.0, 12.0, 11.0, 10.0, 12.0, 13.0]);
|
||||
let l = make_arr(&[8.0, 9.0, 10.0, 9.0, 8.0, 10.0, 11.0]);
|
||||
let c = make_arr(&[9.0, 10.0, 11.0, 10.0, 9.0, 11.0, 12.0]);
|
||||
let out = stochf(&h, &l, &c, 3, 2);
|
||||
assert_eq!(out.length(), 2);
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_stochf_output_length() {
|
||||
let h = make_arr(&[10.0, 11.0, 12.0, 11.0, 10.0, 12.0, 13.0]);
|
||||
let l = make_arr(&[8.0, 9.0, 10.0, 9.0, 8.0, 10.0, 11.0]);
|
||||
let c = make_arr(&[9.0, 10.0, 11.0, 10.0, 9.0, 11.0, 12.0]);
|
||||
let out = stochf(&h, &l, &c, 3, 2);
|
||||
let fastk = Float64Array::from(out.get(0));
|
||||
assert_eq!(fastk.length(), 7);
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_stochf_fastk_in_0_to_100() {
|
||||
let h = make_arr(&[10.0, 11.0, 12.0, 11.0, 10.0, 12.0, 13.0]);
|
||||
let l = make_arr(&[8.0, 9.0, 10.0, 9.0, 8.0, 10.0, 11.0]);
|
||||
let c = make_arr(&[9.0, 10.0, 11.0, 10.0, 9.0, 11.0, 12.0]);
|
||||
let out = stochf(&h, &l, &c, 3, 2);
|
||||
let fastk = Float64Array::from(out.get(0));
|
||||
let finite = get_finite(&fastk);
|
||||
assert!(!finite.is_empty(), "fastk should have finite values");
|
||||
for v in finite {
|
||||
assert!(v >= 0.0 && v <= 100.0, "fastk value {v} out of [0, 100]");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user