feat: init the repo

This commit is contained in:
Pratik Bhadane
2026-03-23 23:34:28 +05:30
commit 7a5a220dfe
344 changed files with 75728 additions and 0 deletions
+415
View File
@@ -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"
+22
View File
@@ -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
View File
@@ -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).
+24
View File
@@ -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
View File
@@ -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]");
}
}
}