Compare commits

..

5 Commits

Author SHA1 Message Date
kingchenc d2bc000892 release: bump 0.5.7 -> 0.5.8 (#185)
Version bump for the B4 price oscillators release (#184): `TsfOscillator`, `MacdHistogram`, `PpoHistogram` — 420 → 423 indicators.

Bumps workspace + bindings (Cargo.toml/lock, pyproject, node package.json + 6 platform manifests + lockfiles) and rolls CHANGELOG `[Unreleased]` into `[0.5.8]`.
2026-06-04 19:47:22 +02:00
kingchenc 1f4bf9e3a6 feat(core): B4 price oscillators (TsfOscillator, MacdHistogram, PpoHistogram) (#184)
Adds three **Price Oscillators** family indicators (420 → 423).

## Indicators

- **TsfOscillator** — `100·(close − TSF)/close`, the percentage gap of the close to the **one-bar-ahead** time-series forecast. Close-relative companion to `Cfo`, which measures the same gap against the regression value at the *current* bar; the two differ by exactly the slope term `100·b/close`.
- **MacdHistogram** — the standalone `macd − signal` bar of MACD exposed as a plain `f64` series.
- **PpoHistogram** — the Percentage Price Oscillator with its 9-period signal EMA and the resulting scale-free, zero-centered histogram (PPO itself only emits the line).

All three are scalar `f64` indicators wrapping existing, already-tested building blocks (`MacdIndicator`, `Ppo` + `Ema`, `Tsf`).

## Scope notes (VORAB-CHECK)

The B4 roadmap listed six items; three were dropped to avoid duplicates:
- *Forecast Oscillator* already ships as `Cfo`.
- *Derivative Oscillator* already ships (`DerivativeOscillator`, B2).
- *Detrended Synthetic Price* deferred — no citable formula distinct from the existing `Apo`/`Dpo`.

## Touchpoints

Core (`tsf_oscillator.rs`, `macd_histogram.rs`, `ppo_histogram.rs`) with full per-branch unit tests, `mod.rs`/`lib.rs`, python/node/wasm bindings (wasm via typed-arg macro, python/node hand-written for the multi-arg histograms), fuzz drivers, python reference + streaming-vs-batch tests, node factories, README family row + counter, CHANGELOG.

Local verify: `cargo test --workspace` green, `clippy -D warnings` clean, node 498 tests, full python suite green.
2026-06-04 19:36:43 +02:00
kingchenc d36d514f56 fix(core): re-export GatorOscillatorOutput & KasePermissionStochasticOutput (#183)
The two market-profile struct-output indicators from the B3 batch (`GatorOscillator`, `KasePermissionStochastic`) exposed their public output structs from their own modules but did not re-export them from `indicators` / the crate root — unlike every other struct-output indicator (`ElderRayOutput`, `AlligatorOutput`, `QqeOutput`, …).

That left `wickra::GatorOscillatorOutput` / `wickra::KasePermissionStochasticOutput` un-nameable, so Rust callers could not annotate or store the `update` result by type. Surfaced by the wickra-docs Rust-snippet-compile check on the B3 deep-dives.

Re-export both alongside their structs. Both names end in `Output`, so the indicator counter strips them — catalog count stays **420**.
2026-06-04 18:43:26 +02:00
kingchenc 6e0464930e release: bump 0.5.6 -> 0.5.7 (#182)
Version bump 0.5.6 → 0.5.7 for the **B3 — Trend & Directional** batch (#181):
seven new indicators (`Qstick`, `TtmTrend`, `TrendStrengthIndex`,
`PolarizedFractalEfficiency`, `WavePm`, `GatorOscillator`,
`KasePermissionStochastic`), catalog 413 → 420.

Bumps: workspace `Cargo.toml` + `Cargo.lock`, `bindings/python/pyproject.toml`,
`bindings/node/package.json` + the six `npm/*/package.json` platform manifests,
both `package-lock.json` files, and the `CHANGELOG.md` `[Unreleased]` → `[0.5.7]`
roll with compare URLs.
2026-06-04 18:06:57 +02:00
kingchenc 13bc801f89 feat(indicators): B3 Trend & Directional batch (413 -> 420) (#181)
Adds the **B3 — Trend & Directional** batch: seven new indicators, taking the
catalog from 413 to 420 (Trend & Directional family).

| Indicator | Input → Output | Summary |
|-----------|----------------|---------|
| `Qstick` | candle → f64 | Chande's SMA of the candle body (close − open) |
| `TtmTrend` | candle → f64 (±1) | John Carter close-vs-median-SMA trend filter |
| `TrendStrengthIndex` | f64 → f64 | signed r² of an OLS regression of price vs time |
| `PolarizedFractalEfficiency` | f64 → f64 | Hannula directional trend efficiency |
| `WavePm` | f64 → f64 | Kase variance-normalised peak-momentum statistic (reconstruction) |
| `GatorOscillator` | candle → struct | Bill Williams Alligator convergence/divergence histogram |
| `KasePermissionStochastic` | candle → struct | double-smoothed stochastic permission filter |

Note: the roadmap's "Directional Indicator +DI/−DI" item is already covered by
the existing standalone `PlusDi` / `MinusDi` / `Dx`, so it is intentionally not
re-added.

All touchpoints wired: core (every-branch unit tests), Python/Node/WASM
bindings, fuzz drivers, Python test registries + reference tests, Node
factories, README/CHANGELOG counters.

Local verify: `cargo test -p wickra-core` (lib 3389 + doc 378), `cargo clippy
--workspace --all-targets --all-features -- -D warnings`, node build + 495
tests, maturin + 815 pytest, counter 420 == 420.
2026-06-04 17:57:24 +02:00
37 changed files with 3694 additions and 114 deletions
+17 -1
View File
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.5.8] - 2026-06-04
- **TSF Oscillator** — the percentage gap of the close to the one-bar-ahead time-series forecast, a close-relative companion to CFO (`TsfOscillator`).
- **MACD Histogram** — the standalone macd-minus-signal bar of MACD as a scalar series (`MacdHistogram`).
- **PPO Histogram** — the Percentage Price Oscillator with its signal EMA and the resulting zero-centered histogram (`PpoHistogram`).
## [0.5.7] - 2026-06-04
- **Qstick** — Qstick (Chande), the SMA of the candle body (close open) as a net buying/selling pressure gauge (`QSTICK`).
- **TTM Trend** — TTM Trend (John Carter), +1/1 by whether the close sits above the SMA of recent median prices (`TTM_TREND`).
- **Trend Strength Index** — trend strength index, the signed r² of a linear regression of price against time (`TREND_STRENGTH_INDEX`).
- **Polarized Fractal Efficiency** — polarized fractal efficiency (Hannula), directional trend efficiency over a fractal lookback (`POLARIZED_FRACTAL_EFFICIENCY`).
- **Wave PM** — Wave PM (Kase), a variance-normalised peak-momentum statistic (`WAVE_PM`).
- **Gator Oscillator** — Gator Oscillator (Bill Williams), the Alligator convergence/divergence histogram (`GATOR_OSCILLATOR`).
- **Kase Permission Stochastic** — Kase Permission Stochastic, a double-smoothed stochastic used as a trade-permission filter (`KASE_PERMISSION_STOCHASTIC`).
## [0.5.6] - 2026-06-04
- **QQE** — quantitative qualitative estimation, a smoothed RSI with an ATR-of-RSI trailing line (`QQE`).
- **Intraday Momentum Index** — intraday momentum index (Chande), RSI on the open-to-close body (`IMI`).
@@ -1259,7 +1273,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
optional Binance live feed.
- Bindings for Python, Node.js, and WebAssembly.
[Unreleased]: https://github.com/wickra-lib/wickra/compare/v0.5.6...HEAD
[Unreleased]: https://github.com/wickra-lib/wickra/compare/v0.5.8...HEAD
[0.5.8]: https://github.com/wickra-lib/wickra/compare/v0.5.7...v0.5.8
[0.5.7]: https://github.com/wickra-lib/wickra/compare/v0.5.6...v0.5.7
[0.5.6]: https://github.com/wickra-lib/wickra/compare/v0.5.5...v0.5.6
[0.5.5]: https://github.com/wickra-lib/wickra/compare/v0.5.4...v0.5.5
[0.5.4]: https://github.com/wickra-lib/wickra/compare/v0.5.3...v0.5.4
Generated
+6 -6
View File
@@ -1867,7 +1867,7 @@ dependencies = [
[[package]]
name = "wickra"
version = "0.5.6"
version = "0.5.8"
dependencies = [
"approx",
"criterion",
@@ -1878,7 +1878,7 @@ dependencies = [
[[package]]
name = "wickra-core"
version = "0.5.6"
version = "0.5.8"
dependencies = [
"approx",
"proptest",
@@ -1888,7 +1888,7 @@ dependencies = [
[[package]]
name = "wickra-data"
version = "0.5.6"
version = "0.5.8"
dependencies = [
"approx",
"csv",
@@ -1915,7 +1915,7 @@ dependencies = [
[[package]]
name = "wickra-node"
version = "0.5.6"
version = "0.5.8"
dependencies = [
"napi",
"napi-build",
@@ -1925,7 +1925,7 @@ dependencies = [
[[package]]
name = "wickra-python"
version = "0.5.6"
version = "0.5.8"
dependencies = [
"numpy",
"pyo3",
@@ -1934,7 +1934,7 @@ dependencies = [
[[package]]
name = "wickra-wasm"
version = "0.5.6"
version = "0.5.8"
dependencies = [
"console_error_panic_hook",
"js-sys",
+2 -2
View File
@@ -12,7 +12,7 @@ members = [
exclude = ["fuzz"]
[workspace.package]
version = "0.5.6"
version = "0.5.8"
authors = ["kingchenc <support@wickra.org>"]
edition = "2021"
rust-version = "1.86"
@@ -24,7 +24,7 @@ keywords = ["finance", "trading", "indicators", "technical-analysis", "ta"]
categories = ["finance", "mathematics", "science"]
[workspace.dependencies]
wickra-core = { path = "crates/wickra-core", version = "0.5.6" }
wickra-core = { path = "crates/wickra-core", version = "0.5.8" }
thiserror = "2"
rayon = "1.10"
+6 -6
View File
@@ -1,5 +1,5 @@
<p align="center">
<a href="https://wickra.org"><img src="https://raw.githubusercontent.com/wickra-lib/.github/main/profile/wickra-banner.webp?v=413" alt="Wickra — streaming-first technical indicators" width="100%"></a>
<a href="https://wickra.org"><img src="https://raw.githubusercontent.com/wickra-lib/.github/main/profile/wickra-banner.webp?v=423" alt="Wickra — streaming-first technical indicators" width="100%"></a>
</p>
[![CI](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml/badge.svg)](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml)
@@ -48,7 +48,7 @@ Full documentation lives at **[docs.wickra.org](https://docs.wickra.org)**:
[Node](https://docs.wickra.org/Quickstart-Node),
[WASM](https://docs.wickra.org/Quickstart-WASM).
- **Indicators** — a per-indicator deep dive (formula, parameters, warmup) for
every one of the 413 indicators; start at the
every one of the 423 indicators; start at the
[indicators overview](https://docs.wickra.org/Indicators-Overview).
- **Reference** — [warmup periods](https://docs.wickra.org/Warmup-Periods),
[streaming vs batch](https://docs.wickra.org/Streaming-vs-Batch),
@@ -136,7 +136,7 @@ python -m benchmarks.compare_libraries
## Indicators
413 streaming-first indicators across twenty-four families. Every one passes the
423 streaming-first indicators across twenty-four families. Every one passes the
`batch == streaming` equivalence test, reference-value tests, and reset
semantics tests. Each has a per-indicator deep dive (formula, parameters,
warmup) at [docs.wickra.org](https://docs.wickra.org/Indicators-Overview).
@@ -145,8 +145,8 @@ warmup) at [docs.wickra.org](https://docs.wickra.org/Indicators-Overview).
|--------|-----------|
| Moving Averages | SMA, EMA, WMA, DEMA, TEMA, HMA, KAMA, SMMA, TRIMA, ZLEMA, T3, VWMA, ALMA, McGinley Dynamic, FRAMA, VIDYA, JMA, Alligator, EVWMA, SWMA, GMA, EHMA, Median MA, Adaptive Laguerre, GD, Holt-Winters |
| Momentum Oscillators | RSI (Wilder), Anchored RSI, Stochastic, CCI, ROC, Williams %R, MFI, Awesome Oscillator, MOM, CMO, TSI, PMO, StochRSI, Ultimate Oscillator, RVI, PGO, KST, SMI, Laguerre RSI, Connors RSI, Inertia, ROC Percentage (ROCP), ROC Ratio (ROCR), ROC Ratio 100 (ROCR100), Disparity Index, Fisher RSI, RSX, Dynamic Momentum Index, Stochastic CCI, RMI, Derivative Oscillator, Elder Ray, Intraday Momentum Index, QQE |
| Trend & Directional | MACD, MACD Fixed (MACDFIX), MACD Extended (MACDEXT), ADX (+DI/-DI), ADXR, Aroon, TRIX, Aroon Oscillator, Vortex, Random Walk Index, Trend Intensity Index, Wave Trend Oscillator, Mass Index, Choppiness Index, Vertical Horizontal Filter, Plus DM, Minus DM, Plus DI, Minus DI, DX |
| Price Oscillators | PPO, DPO, Coppock, Accelerator Oscillator, Balance of Power, APO, AO Histogram, CFO, Zero-Lag MACD, Elder Impulse, STC |
| Trend & Directional | MACD, MACD Fixed (MACDFIX), MACD Extended (MACDEXT), ADX (+DI/-DI), ADXR, Aroon, TRIX, Aroon Oscillator, Vortex, Random Walk Index, Trend Intensity Index, Wave Trend Oscillator, Mass Index, Choppiness Index, Vertical Horizontal Filter, Plus DM, Minus DM, Plus DI, Minus DI, DX, TTM Trend, Trend Strength Index, Qstick, Polarized Fractal Efficiency, Wave PM, Gator Oscillator, Kase Permission Stochastic |
| Price Oscillators | PPO, DPO, Coppock, Accelerator Oscillator, Balance of Power, APO, AO Histogram, CFO, Zero-Lag MACD, Elder Impulse, STC, TSF Oscillator, MACD Histogram, PPO Histogram |
| Volatility & Bands | ATR, Bollinger Bands, Keltner Channels, Donchian Channels, NATR, StdDev, Ulcer Index, Historical Volatility, Bollinger Bandwidth, %B, True Range, Chaikin Volatility, RVI (Relative Volatility Index), Parkinson Volatility, Garman-Klass Volatility, Rogers-Satchell Volatility, Yang-Zhang Volatility |
| Bands & Channels | MA Envelope, Acceleration Bands, STARC Bands, ATR Bands, Hurst Channel, LinReg Channel, Standard Error Bands, Double Bollinger Bands, TTM Squeeze, Fractal Chaos Bands, VWAP StdDev Bands |
| Trailing Stops | Parabolic SAR, Parabolic SAR Extended (SAREXT), SuperTrend, Chandelier Exit, Chande Kroll Stop, ATR Trailing Stop, HiLo Activator, Volty Stop, Yo-Yo Exit, Donchian Channel Stop, Percentage Trailing Stop, Step Trailing Stop, Renko Trailing Stop |
@@ -245,7 +245,7 @@ A Python live-trading example using the public `websockets` package lives at
```
wickra/
├── crates/
│ ├── wickra-core/ core engine + all 413 indicators
│ ├── wickra-core/ core engine + all 423 indicators
│ ├── wickra/ top-level facade crate (publishes on crates.io) + benches/
│ └── wickra-data/ CSV reader, tick aggregator, live exchange feeds
├── bindings/
@@ -28,6 +28,12 @@ function num(v) {
// --- Scalar indicators: update(value) vs batch(prices) ---
const scalarFactories = {
PpoHistogram: () => new wickra.PpoHistogram(3, 6, 3),
MacdHistogram: () => new wickra.MacdHistogram(3, 6, 3),
TsfOscillator: () => new wickra.TsfOscillator(3),
WAVE_PM: () => new wickra.WAVE_PM(32, 3),
POLARIZED_FRACTAL_EFFICIENCY: () => new wickra.POLARIZED_FRACTAL_EFFICIENCY(10, 5),
TREND_STRENGTH_INDEX: () => new wickra.TREND_STRENGTH_INDEX(20),
DerivativeOscillator: () => new wickra.DerivativeOscillator(14, 5, 3, 9),
RMI: () => new wickra.RMI(14, 5),
DynamicMomentumIndex: () => new wickra.DynamicMomentumIndex(14),
@@ -342,6 +348,8 @@ const candleScalar = {
HighLowRange: { make: () => new wickra.HighLowRange(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
StochasticCCI: { make: () => new wickra.StochasticCCI(14), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
IMI: { make: () => new wickra.IMI(14), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
TTM_TREND: { make: () => new wickra.TTM_TREND(6), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
Qstick: { make: () => new wickra.Qstick(10), step: (ind, i) => ind.update(open[i], close[i]), batch: (ind) => ind.batch(open, close) },
};
for (const [name, d] of Object.entries(candleScalar)) {
@@ -426,6 +434,8 @@ const multi = {
FibTimeZones: { make: () => new wickra.FibTimeZones(), fields: ['onZone', 'barsToNext'], step: (ind, i) => ind.update(high[i], low[i]), batch: (ind) => ind.batch(high, low) },
ElderRay: { make: () => new wickra.ElderRay(13), fields: ['bullPower', 'bearPower'], step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
QQE: { make: () => new wickra.QQE(14, 5, 4.236), fields: ['rsiMa', 'trailingLine'], step: (ind, i) => ind.update(close[i]), batch: (ind) => ind.batch(close) },
GatorOscillator: { make: () => new wickra.GatorOscillator(13, 8, 5), fields: ['upper', 'lower'], step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
KasePermissionStochastic: { make: () => new wickra.KasePermissionStochastic(9, 3), fields: ['fast', 'slow'], step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
};
for (const [name, d] of Object.entries(multi)) {
+98
View File
@@ -77,6 +77,14 @@ export interface ElderRayValue {
bullPower: number
bearPower: number
}
export interface GatorOscillatorValue {
upper: number
lower: number
}
export interface KasePermissionStochasticValue {
fast: number
slow: number
}
export interface StochValue {
k: number
d: number
@@ -961,6 +969,24 @@ export declare class DynamicMomentumIndex {
isReady(): boolean
warmupPeriod(): number
}
export type TrendStrengthIndexNode = TREND_STRENGTH_INDEX
export declare class TREND_STRENGTH_INDEX {
constructor(period: number)
update(value: number): number | null
batch(prices: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type TsfOscillatorNode = TsfOscillator
export declare class TsfOscillator {
constructor(period: number)
update(value: number): number | null
batch(prices: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type JumpIndicatorNode = JumpIndicator
export declare class JumpIndicator {
constructor(period: number, threshold: number)
@@ -1494,6 +1520,60 @@ export declare class ElderRay {
isReady(): boolean
warmupPeriod(): number
}
export type TtmTrendNode = TTM_TREND
export declare class TTM_TREND {
constructor(period: number)
update(high: number, low: number, close: number): number | null
batch(high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type QstickNode = Qstick
export declare class Qstick {
constructor(period: number)
update(open: number, close: number): number | null
batch(open: Array<number>, close: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type PolarizedFractalEfficiencyNode = POLARIZED_FRACTAL_EFFICIENCY
export declare class POLARIZED_FRACTAL_EFFICIENCY {
constructor(period: number, smoothing: number)
update(value: number): number | null
batch(prices: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type WavePmNode = WAVE_PM
export declare class WAVE_PM {
constructor(length: number, smoothing: number)
update(value: number): number | null
batch(prices: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type GatorOscillatorNode = GatorOscillator
export declare class GatorOscillator {
constructor(jawPeriod: number, teethPeriod: number, lipsPeriod: number)
update(high: number, low: number, close: number): GatorOscillatorValue | null
batch(high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type KasePermissionStochasticNode = KasePermissionStochastic
export declare class KasePermissionStochastic {
constructor(length: number, smooth: number)
update(high: number, low: number, close: number): KasePermissionStochasticValue | null
batch(high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type StochNode = Stochastic
export declare class Stochastic {
constructor(kPeriod: number, dPeriod: number)
@@ -1838,6 +1918,24 @@ export declare class DerivativeOscillator {
isReady(): boolean
warmupPeriod(): number
}
export type MacdHistogramNode = MacdHistogram
export declare class MacdHistogram {
constructor(fast: number, slow: number, signal: number)
update(value: number): number | null
batch(prices: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type PpoHistogramNode = PpoHistogram
export declare class PpoHistogram {
constructor(fast: number, slow: number, signal: number)
update(value: number): number | null
batch(prices: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type TsiNode = TSI
export declare class TSI {
constructor(long: number, short: number)
+11 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "wickra-darwin-arm64",
"version": "0.5.6",
"version": "0.5.8",
"description": "Native binding for wickra (macOS Apple Silicon). Installed automatically as an optional dependency of wickra on matching platforms.",
"main": "wickra.darwin-arm64.node",
"files": [
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "wickra-darwin-x64",
"version": "0.5.6",
"version": "0.5.8",
"description": "Native binding for wickra (macOS Intel). Installed automatically as an optional dependency of wickra on matching platforms.",
"main": "wickra.darwin-x64.node",
"files": [
@@ -1,6 +1,6 @@
{
"name": "wickra-linux-arm64-gnu",
"version": "0.5.6",
"version": "0.5.8",
"description": "Native binding for wickra (linux arm64 GNU). Installed automatically as an optional dependency of wickra on matching platforms.",
"main": "wickra.linux-arm64-gnu.node",
"files": [
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "wickra-linux-x64-gnu",
"version": "0.5.6",
"version": "0.5.8",
"description": "Native binding for wickra (linux x64 GNU). Installed automatically as an optional dependency of wickra on matching platforms.",
"main": "wickra.linux-x64-gnu.node",
"files": [
@@ -1,6 +1,6 @@
{
"name": "wickra-win32-arm64-msvc",
"version": "0.5.6",
"version": "0.5.8",
"description": "Native binding for wickra (Windows arm64 MSVC). Installed automatically as an optional dependency of wickra on matching platforms.",
"main": "wickra.win32-arm64-msvc.node",
"files": [
@@ -1,6 +1,6 @@
{
"name": "wickra-win32-x64-msvc",
"version": "0.5.6",
"version": "0.5.8",
"description": "Native binding for wickra (Windows x64 MSVC). Installed automatically as an optional dependency of wickra on matching platforms.",
"main": "wickra.win32-x64-msvc.node",
"files": [
+20 -20
View File
@@ -1,12 +1,12 @@
{
"name": "wickra",
"version": "0.5.6",
"version": "0.5.8",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "wickra",
"version": "0.5.6",
"version": "0.5.8",
"license": "MIT OR Apache-2.0",
"devDependencies": {
"@napi-rs/cli": "^2.18.0"
@@ -15,12 +15,12 @@
"node": ">= 18"
},
"optionalDependencies": {
"wickra-darwin-arm64": "0.5.6",
"wickra-darwin-x64": "0.5.6",
"wickra-linux-arm64-gnu": "0.5.6",
"wickra-linux-x64-gnu": "0.5.6",
"wickra-win32-arm64-msvc": "0.5.6",
"wickra-win32-x64-msvc": "0.5.6"
"wickra-darwin-arm64": "0.5.8",
"wickra-darwin-x64": "0.5.8",
"wickra-linux-arm64-gnu": "0.5.8",
"wickra-linux-x64-gnu": "0.5.8",
"wickra-win32-arm64-msvc": "0.5.8",
"wickra-win32-x64-msvc": "0.5.8"
}
},
"node_modules/@napi-rs/cli": {
@@ -41,8 +41,8 @@
}
},
"node_modules/wickra-darwin-arm64": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/wickra-darwin-arm64/-/wickra-darwin-arm64-0.5.6.tgz",
"version": "0.5.8",
"resolved": "https://registry.npmjs.org/wickra-darwin-arm64/-/wickra-darwin-arm64-0.5.8.tgz",
"integrity": "sha512-4eZiBR/yGUdr4nzhEUFy2i69XgNx64iI2ax/LPamsThgylC0KpHOZKK19QzJ2d9KbK4C8nMjME5FLuR+4GNEwQ==",
"cpu": [
"arm64"
@@ -57,8 +57,8 @@
}
},
"node_modules/wickra-darwin-x64": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/wickra-darwin-x64/-/wickra-darwin-x64-0.5.6.tgz",
"version": "0.5.8",
"resolved": "https://registry.npmjs.org/wickra-darwin-x64/-/wickra-darwin-x64-0.5.8.tgz",
"integrity": "sha512-6hf8zI3QPjTFp4zCpmgUwDvNtu6jHqNUHKD5e55POo0CgA52HkpyxSPtVm8TGTIZDI7kPjlbOdBM8CJ76mmXwA==",
"cpu": [
"x64"
@@ -73,8 +73,8 @@
}
},
"node_modules/wickra-linux-arm64-gnu": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/wickra-linux-arm64-gnu/-/wickra-linux-arm64-gnu-0.5.6.tgz",
"version": "0.5.8",
"resolved": "https://registry.npmjs.org/wickra-linux-arm64-gnu/-/wickra-linux-arm64-gnu-0.5.8.tgz",
"integrity": "sha512-kSe6y0xBMSiqdPLXNjwop5WZdHtvdBNKSEBCwZ4hFq33p4apW25/wrlzv9/oDuyD4kuPabJEhCCnFOplh58CUg==",
"cpu": [
"arm64"
@@ -89,8 +89,8 @@
}
},
"node_modules/wickra-linux-x64-gnu": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/wickra-linux-x64-gnu/-/wickra-linux-x64-gnu-0.5.6.tgz",
"version": "0.5.8",
"resolved": "https://registry.npmjs.org/wickra-linux-x64-gnu/-/wickra-linux-x64-gnu-0.5.8.tgz",
"integrity": "sha512-tWBWS4qz7hxM4xnpFb59bhf6TaLwXq0Z3jEa/2l7r8PiHA94g8r8S53NRMiT+4yiL5hSWe/nUiC/YXdRrhEZ4g==",
"cpu": [
"x64"
@@ -105,8 +105,8 @@
}
},
"node_modules/wickra-win32-arm64-msvc": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/wickra-win32-arm64-msvc/-/wickra-win32-arm64-msvc-0.5.6.tgz",
"version": "0.5.8",
"resolved": "https://registry.npmjs.org/wickra-win32-arm64-msvc/-/wickra-win32-arm64-msvc-0.5.8.tgz",
"integrity": "sha512-EXIckHxAtF75PUGDKRzXyqMe9ldP0JjSdu68WFN6iJfp+McYrGu6h40TEJlQ/oUEIoPqiZB/xhVyo/el5Lg7zw==",
"cpu": [
"arm64"
@@ -121,8 +121,8 @@
}
},
"node_modules/wickra-win32-x64-msvc": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/wickra-win32-x64-msvc/-/wickra-win32-x64-msvc-0.5.6.tgz",
"version": "0.5.8",
"resolved": "https://registry.npmjs.org/wickra-win32-x64-msvc/-/wickra-win32-x64-msvc-0.5.8.tgz",
"integrity": "sha512-Yfsqq1Xwp6hdxMyLze411vNdo7BDwI6+lPSe7A9XdqyPecNDbtKwYLpsal2r8EHbNzqM+R8XnuRtUaEQS5VlUQ==",
"cpu": [
"x64"
+7 -7
View File
@@ -1,6 +1,6 @@
{
"name": "wickra",
"version": "0.5.6",
"version": "0.5.8",
"description": "Streaming-first technical indicators: incremental, fast, install-free. Node bindings powered by Rust.",
"author": "kingchenc <support@wickra.org>",
"main": "index.js",
@@ -47,12 +47,12 @@
"node": ">= 18"
},
"optionalDependencies": {
"wickra-linux-x64-gnu": "0.5.6",
"wickra-linux-arm64-gnu": "0.5.6",
"wickra-darwin-x64": "0.5.6",
"wickra-darwin-arm64": "0.5.6",
"wickra-win32-x64-msvc": "0.5.6",
"wickra-win32-arm64-msvc": "0.5.6"
"wickra-linux-x64-gnu": "0.5.8",
"wickra-linux-arm64-gnu": "0.5.8",
"wickra-darwin-x64": "0.5.8",
"wickra-darwin-arm64": "0.5.8",
"wickra-win32-x64-msvc": "0.5.8",
"wickra-win32-arm64-msvc": "0.5.8"
},
"scripts": {
"build": "napi build --platform --release",
+404
View File
@@ -214,6 +214,12 @@ node_scalar_indicator!(
"DynamicMomentumIndex",
wc::DynamicMomentumIndex
);
node_scalar_indicator!(
TrendStrengthIndexNode,
"TREND_STRENGTH_INDEX",
wc::TrendStrengthIndex
);
node_scalar_indicator!(TsfOscillatorNode, "TsfOscillator", wc::TsfOscillator);
#[napi(js_name = "JumpIndicator")]
pub struct JumpIndicatorNode {
inner: wc::JumpIndicator,
@@ -2295,6 +2301,328 @@ impl ElderRayNode {
}
}
#[napi(js_name = "TTM_TREND")]
pub struct TtmTrendNode {
inner: wc::TtmTrend,
}
#[napi]
impl TtmTrendNode {
#[napi(constructor)]
pub fn new(period: u32) -> napi::Result<Self> {
Ok(Self {
inner: wc::TtmTrend::new(period as usize).map_err(map_err)?,
})
}
#[napi]
pub fn update(&mut self, high: f64, low: f64, close: f64) -> napi::Result<Option<f64>> {
Ok(self.inner.update(cnd(high, low, close, 0.0)?))
}
#[napi]
pub fn batch(
&mut self,
high: Vec<f64>,
low: Vec<f64>,
close: Vec<f64>,
) -> napi::Result<Vec<f64>> {
if high.len() != low.len() || low.len() != close.len() {
return Err(NapiError::from_reason(
"high, low, close must be equal length".to_string(),
));
}
let mut out = Vec::with_capacity(high.len());
for i in 0..high.len() {
out.push(
self.inner
.update(cnd(high[i], low[i], close[i], 0.0)?)
.unwrap_or(f64::NAN),
);
}
Ok(out)
}
#[napi]
pub fn reset(&mut self) {
self.inner.reset();
}
#[napi(js_name = "isReady")]
pub fn is_ready(&self) -> bool {
self.inner.is_ready()
}
#[napi(js_name = "warmupPeriod")]
pub fn warmup_period(&self) -> u32 {
self.inner.warmup_period() as u32
}
}
#[napi(js_name = "Qstick")]
pub struct QstickNode {
inner: wc::Qstick,
}
#[napi]
impl QstickNode {
#[napi(constructor)]
pub fn new(period: u32) -> napi::Result<Self> {
Ok(Self {
inner: wc::Qstick::new(period as usize).map_err(map_err)?,
})
}
#[napi]
pub fn update(&mut self, open: f64, close: f64) -> napi::Result<Option<f64>> {
let hi = open.max(close);
let lo = open.min(close);
Ok(self.inner.update(cnd4(open, hi, lo, close)?))
}
#[napi]
pub fn batch(&mut self, open: Vec<f64>, close: Vec<f64>) -> napi::Result<Vec<f64>> {
if open.len() != close.len() {
return Err(NapiError::from_reason(
"open, close must be equal length".to_string(),
));
}
let mut out = Vec::with_capacity(open.len());
for i in 0..open.len() {
let hi = open[i].max(close[i]);
let lo = open[i].min(close[i]);
out.push(
self.inner
.update(cnd4(open[i], hi, lo, close[i])?)
.unwrap_or(f64::NAN),
);
}
Ok(out)
}
#[napi]
pub fn reset(&mut self) {
self.inner.reset();
}
#[napi(js_name = "isReady")]
pub fn is_ready(&self) -> bool {
self.inner.is_ready()
}
#[napi(js_name = "warmupPeriod")]
pub fn warmup_period(&self) -> u32 {
self.inner.warmup_period() as u32
}
}
#[napi(js_name = "POLARIZED_FRACTAL_EFFICIENCY")]
pub struct PolarizedFractalEfficiencyNode {
inner: wc::PolarizedFractalEfficiency,
}
#[napi]
impl PolarizedFractalEfficiencyNode {
#[napi(constructor)]
pub fn new(period: u32, smoothing: u32) -> napi::Result<Self> {
Ok(Self {
inner: wc::PolarizedFractalEfficiency::new(period as usize, smoothing as usize)
.map_err(map_err)?,
})
}
#[napi]
pub fn update(&mut self, value: f64) -> Option<f64> {
self.inner.update(value)
}
#[napi]
pub fn batch(&mut self, prices: Vec<f64>) -> Vec<f64> {
flatten(self.inner.batch(&prices))
}
#[napi]
pub fn reset(&mut self) {
self.inner.reset();
}
#[napi(js_name = "isReady")]
pub fn is_ready(&self) -> bool {
self.inner.is_ready()
}
#[napi(js_name = "warmupPeriod")]
pub fn warmup_period(&self) -> u32 {
self.inner.warmup_period() as u32
}
}
#[napi(js_name = "WAVE_PM")]
pub struct WavePmNode {
inner: wc::WavePm,
}
#[napi]
impl WavePmNode {
#[napi(constructor)]
pub fn new(length: u32, smoothing: u32) -> napi::Result<Self> {
Ok(Self {
inner: wc::WavePm::new(length as usize, smoothing as usize).map_err(map_err)?,
})
}
#[napi]
pub fn update(&mut self, value: f64) -> Option<f64> {
self.inner.update(value)
}
#[napi]
pub fn batch(&mut self, prices: Vec<f64>) -> Vec<f64> {
flatten(self.inner.batch(&prices))
}
#[napi]
pub fn reset(&mut self) {
self.inner.reset();
}
#[napi(js_name = "isReady")]
pub fn is_ready(&self) -> bool {
self.inner.is_ready()
}
#[napi(js_name = "warmupPeriod")]
pub fn warmup_period(&self) -> u32 {
self.inner.warmup_period() as u32
}
}
#[napi(object)]
pub struct GatorOscillatorValue {
pub upper: f64,
pub lower: f64,
}
#[napi(js_name = "GatorOscillator")]
pub struct GatorOscillatorNode {
inner: wc::GatorOscillator,
}
#[napi]
impl GatorOscillatorNode {
#[napi(constructor)]
pub fn new(jaw_period: u32, teeth_period: u32, lips_period: u32) -> napi::Result<Self> {
Ok(Self {
inner: wc::GatorOscillator::new(
jaw_period as usize,
teeth_period as usize,
lips_period as usize,
)
.map_err(map_err)?,
})
}
#[napi]
pub fn update(
&mut self,
high: f64,
low: f64,
close: f64,
) -> napi::Result<Option<GatorOscillatorValue>> {
Ok(self
.inner
.update(cnd(high, low, close, 0.0)?)
.map(|o| GatorOscillatorValue {
upper: o.upper,
lower: o.lower,
}))
}
#[napi]
pub fn batch(
&mut self,
high: Vec<f64>,
low: Vec<f64>,
close: Vec<f64>,
) -> napi::Result<Vec<f64>> {
if high.len() != low.len() || low.len() != close.len() {
return Err(NapiError::from_reason(
"high, low, close must be equal length".to_string(),
));
}
let n = high.len();
let mut out = vec![f64::NAN; n * 2];
for i in 0..n {
if let Some(o) = self.inner.update(cnd(high[i], low[i], close[i], 0.0)?) {
out[i * 2] = o.upper;
out[i * 2 + 1] = o.lower;
}
}
Ok(out)
}
#[napi]
pub fn reset(&mut self) {
self.inner.reset();
}
#[napi(js_name = "isReady")]
pub fn is_ready(&self) -> bool {
self.inner.is_ready()
}
#[napi(js_name = "warmupPeriod")]
pub fn warmup_period(&self) -> u32 {
self.inner.warmup_period() as u32
}
}
#[napi(object)]
pub struct KasePermissionStochasticValue {
pub fast: f64,
pub slow: f64,
}
#[napi(js_name = "KasePermissionStochastic")]
pub struct KasePermissionStochasticNode {
inner: wc::KasePermissionStochastic,
}
#[napi]
impl KasePermissionStochasticNode {
#[napi(constructor)]
pub fn new(length: u32, smooth: u32) -> napi::Result<Self> {
Ok(Self {
inner: wc::KasePermissionStochastic::new(length as usize, smooth as usize)
.map_err(map_err)?,
})
}
#[napi]
pub fn update(
&mut self,
high: f64,
low: f64,
close: f64,
) -> napi::Result<Option<KasePermissionStochasticValue>> {
Ok(self
.inner
.update(cnd(high, low, close, 0.0)?)
.map(|o| KasePermissionStochasticValue {
fast: o.fast,
slow: o.slow,
}))
}
#[napi]
pub fn batch(
&mut self,
high: Vec<f64>,
low: Vec<f64>,
close: Vec<f64>,
) -> napi::Result<Vec<f64>> {
if high.len() != low.len() || low.len() != close.len() {
return Err(NapiError::from_reason(
"high, low, close must be equal length".to_string(),
));
}
let n = high.len();
let mut out = vec![f64::NAN; n * 2];
for i in 0..n {
if let Some(o) = self.inner.update(cnd(high[i], low[i], close[i], 0.0)?) {
out[i * 2] = o.fast;
out[i * 2 + 1] = o.slow;
}
}
Ok(out)
}
#[napi]
pub fn reset(&mut self) {
self.inner.reset();
}
#[napi(js_name = "isReady")]
pub fn is_ready(&self) -> bool {
self.inner.is_ready()
}
#[napi(js_name = "warmupPeriod")]
pub fn warmup_period(&self) -> u32 {
self.inner.warmup_period() as u32
}
}
#[napi(object)]
pub struct StochValue {
pub k: f64,
@@ -4206,6 +4534,82 @@ impl DerivativeOscillatorNode {
}
}
// ============================== MacdHistogram ==============================
#[napi(js_name = "MacdHistogram")]
pub struct MacdHistogramNode {
inner: wc::MacdHistogram,
}
#[napi]
impl MacdHistogramNode {
#[napi(constructor)]
pub fn new(fast: u32, slow: u32, signal: u32) -> napi::Result<Self> {
Ok(Self {
inner: wc::MacdHistogram::new(fast as usize, slow as usize, signal as usize)
.map_err(map_err)?,
})
}
#[napi]
pub fn update(&mut self, value: f64) -> Option<f64> {
self.inner.update(value)
}
#[napi]
pub fn batch(&mut self, prices: Vec<f64>) -> Vec<f64> {
flatten(self.inner.batch(&prices))
}
#[napi]
pub fn reset(&mut self) {
self.inner.reset();
}
#[napi(js_name = "isReady")]
pub fn is_ready(&self) -> bool {
self.inner.is_ready()
}
#[napi(js_name = "warmupPeriod")]
pub fn warmup_period(&self) -> u32 {
self.inner.warmup_period() as u32
}
}
// ============================== PpoHistogram ==============================
#[napi(js_name = "PpoHistogram")]
pub struct PpoHistogramNode {
inner: wc::PpoHistogram,
}
#[napi]
impl PpoHistogramNode {
#[napi(constructor)]
pub fn new(fast: u32, slow: u32, signal: u32) -> napi::Result<Self> {
Ok(Self {
inner: wc::PpoHistogram::new(fast as usize, slow as usize, signal as usize)
.map_err(map_err)?,
})
}
#[napi]
pub fn update(&mut self, value: f64) -> Option<f64> {
self.inner.update(value)
}
#[napi]
pub fn batch(&mut self, prices: Vec<f64>) -> Vec<f64> {
flatten(self.inner.batch(&prices))
}
#[napi]
pub fn reset(&mut self) {
self.inner.reset();
}
#[napi(js_name = "isReady")]
pub fn is_ready(&self) -> bool {
self.inner.is_ready()
}
#[napi(js_name = "warmupPeriod")]
pub fn warmup_period(&self) -> u32 {
self.inner.warmup_period() as u32
}
}
// ============================== TSI ==============================
#[napi(js_name = "TSI")]
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "maturin"
[project]
name = "wickra"
version = "0.5.6"
version = "0.5.8"
description = "Streaming-first technical indicators: incremental, fast, install-free."
readme = "README.md"
license = "MIT OR Apache-2.0"
+20
View File
@@ -25,6 +25,16 @@ from __future__ import annotations
from ._wickra import (
__version__,
PpoHistogram,
MacdHistogram,
TsfOscillator,
Qstick,
GatorOscillator,
KasePermissionStochastic,
WAVE_PM,
POLARIZED_FRACTAL_EFFICIENCY,
TREND_STRENGTH_INDEX,
TTM_TREND,
QQE,
IMI,
ElderRay,
@@ -466,6 +476,16 @@ from ._wickra import (
)
__all__ = [
"PpoHistogram",
"MacdHistogram",
"TsfOscillator",
"Qstick",
"GatorOscillator",
"KasePermissionStochastic",
"WAVE_PM",
"POLARIZED_FRACTAL_EFFICIENCY",
"TREND_STRENGTH_INDEX",
"TTM_TREND",
"QQE",
"IMI",
"ElderRay",
+577
View File
@@ -2866,6 +2866,573 @@ impl PyStochasticCci {
}
}
// ============================== TtmTrend ==============================
#[pyclass(name = "TTM_TREND", module = "wickra._wickra", skip_from_py_object)]
#[derive(Clone)]
struct PyTtmTrend {
inner: wc::TtmTrend,
}
#[pymethods]
impl PyTtmTrend {
#[new]
#[pyo3(signature = (period=6))]
fn new(period: usize) -> PyResult<Self> {
Ok(Self {
inner: wc::TtmTrend::new(period).map_err(map_err)?,
})
}
fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult<Option<f64>> {
let c = extract_candle(candle)?;
Ok(self.inner.update(c))
}
/// Batch over numpy columns: high, low, close (all 1-D, equal length).
fn batch<'py>(
&mut self,
py: Python<'py>,
high: PyReadonlyArray1<'py, f64>,
low: PyReadonlyArray1<'py, f64>,
close: PyReadonlyArray1<'py, f64>,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
let h = high
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
let l = low
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
let c = close
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
if h.len() != l.len() || l.len() != c.len() {
return Err(PyValueError::new_err(
"high, low, close must be equal length",
));
}
let mut out = Vec::with_capacity(h.len());
for i in 0..h.len() {
let candle = wc::Candle::new(c[i], h[i], l[i], c[i], 0.0, 0).map_err(map_err)?;
out.push(self.inner.update(candle).unwrap_or(f64::NAN));
}
Ok(out.into_pyarray(py))
}
#[getter]
fn period(&self) -> usize {
self.inner.period()
}
fn reset(&mut self) {
self.inner.reset();
}
fn is_ready(&self) -> bool {
self.inner.is_ready()
}
fn warmup_period(&self) -> usize {
self.inner.warmup_period()
}
fn __repr__(&self) -> String {
format!("TTM_TREND(period={})", self.inner.period())
}
}
// ============================== TrendStrengthIndex ==============================
#[pyclass(
name = "TREND_STRENGTH_INDEX",
module = "wickra._wickra",
skip_from_py_object
)]
#[derive(Clone)]
struct PyTrendStrengthIndex {
inner: wc::TrendStrengthIndex,
}
#[pymethods]
impl PyTrendStrengthIndex {
#[new]
#[pyo3(signature = (period=20))]
fn new(period: usize) -> PyResult<Self> {
Ok(Self {
inner: wc::TrendStrengthIndex::new(period).map_err(map_err)?,
})
}
fn update(&mut self, value: f64) -> Option<f64> {
self.inner.update(value)
}
fn batch<'py>(
&mut self,
py: Python<'py>,
prices: PyReadonlyArray1<'py, f64>,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
let s = prices
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
Ok(flatten(self.inner.batch(s)).into_pyarray(py))
}
#[getter]
fn period(&self) -> usize {
self.inner.period()
}
fn reset(&mut self) {
self.inner.reset();
}
fn is_ready(&self) -> bool {
self.inner.is_ready()
}
fn warmup_period(&self) -> usize {
self.inner.warmup_period()
}
fn __repr__(&self) -> String {
format!("TREND_STRENGTH_INDEX(period={})", self.inner.period())
}
}
// ============================== Qstick ==============================
#[pyclass(name = "Qstick", module = "wickra._wickra", skip_from_py_object)]
#[derive(Clone)]
struct PyQstick {
inner: wc::Qstick,
}
#[pymethods]
impl PyQstick {
#[new]
#[pyo3(signature = (period=10))]
fn new(period: usize) -> PyResult<Self> {
Ok(Self {
inner: wc::Qstick::new(period).map_err(map_err)?,
})
}
fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult<Option<f64>> {
let c = extract_candle(candle)?;
Ok(self.inner.update(c))
}
/// Batch over open/close numpy columns (Qstick reads the body close-open).
fn batch<'py>(
&mut self,
py: Python<'py>,
open: PyReadonlyArray1<'py, f64>,
close: PyReadonlyArray1<'py, f64>,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
let o = open
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
let c = close
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
if o.len() != c.len() {
return Err(PyValueError::new_err("open, close must be equal length"));
}
let n = o.len();
let mut out = Vec::with_capacity(n);
for i in 0..n {
let hi = o[i].max(c[i]);
let lo = o[i].min(c[i]);
let candle = wc::Candle::new(o[i], hi, lo, c[i], 0.0, 0).map_err(map_err)?;
out.push(self.inner.update(candle).unwrap_or(f64::NAN));
}
Ok(out.into_pyarray(py))
}
#[getter]
fn period(&self) -> usize {
self.inner.period()
}
fn reset(&mut self) {
self.inner.reset();
}
fn is_ready(&self) -> bool {
self.inner.is_ready()
}
fn warmup_period(&self) -> usize {
self.inner.warmup_period()
}
}
// ============================== PolarizedFractalEfficiency ==============================
#[pyclass(
name = "POLARIZED_FRACTAL_EFFICIENCY",
module = "wickra._wickra",
skip_from_py_object
)]
#[derive(Clone)]
struct PyPolarizedFractalEfficiency {
inner: wc::PolarizedFractalEfficiency,
}
#[pymethods]
impl PyPolarizedFractalEfficiency {
#[new]
#[pyo3(signature = (period=10, smoothing=5))]
fn new(period: usize, smoothing: usize) -> PyResult<Self> {
Ok(Self {
inner: wc::PolarizedFractalEfficiency::new(period, smoothing).map_err(map_err)?,
})
}
fn update(&mut self, value: f64) -> Option<f64> {
self.inner.update(value)
}
fn batch<'py>(
&mut self,
py: Python<'py>,
prices: PyReadonlyArray1<'py, f64>,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
let s = prices
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
Ok(flatten(self.inner.batch(s)).into_pyarray(py))
}
#[getter]
fn period(&self) -> usize {
self.inner.periods().0
}
#[getter]
fn smoothing(&self) -> usize {
self.inner.periods().1
}
fn reset(&mut self) {
self.inner.reset();
}
fn is_ready(&self) -> bool {
self.inner.is_ready()
}
fn warmup_period(&self) -> usize {
self.inner.warmup_period()
}
}
// ============================== WavePm ==============================
#[pyclass(name = "WAVE_PM", module = "wickra._wickra", skip_from_py_object)]
#[derive(Clone)]
struct PyWavePm {
inner: wc::WavePm,
}
#[pymethods]
impl PyWavePm {
#[new]
#[pyo3(signature = (length=32, smoothing=3))]
fn new(length: usize, smoothing: usize) -> PyResult<Self> {
Ok(Self {
inner: wc::WavePm::new(length, smoothing).map_err(map_err)?,
})
}
fn update(&mut self, value: f64) -> Option<f64> {
self.inner.update(value)
}
fn batch<'py>(
&mut self,
py: Python<'py>,
prices: PyReadonlyArray1<'py, f64>,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
let s = prices
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
Ok(flatten(self.inner.batch(s)).into_pyarray(py))
}
#[getter]
fn length(&self) -> usize {
self.inner.periods().0
}
#[getter]
fn smoothing(&self) -> usize {
self.inner.periods().1
}
fn reset(&mut self) {
self.inner.reset();
}
fn is_ready(&self) -> bool {
self.inner.is_ready()
}
fn warmup_period(&self) -> usize {
self.inner.warmup_period()
}
}
// ============================== GatorOscillator ==============================
#[pyclass(
name = "GatorOscillator",
module = "wickra._wickra",
skip_from_py_object
)]
#[derive(Clone)]
struct PyGatorOscillator {
inner: wc::GatorOscillator,
}
#[pymethods]
impl PyGatorOscillator {
#[new]
#[pyo3(signature = (jaw_period=13, teeth_period=8, lips_period=5))]
fn new(jaw_period: usize, teeth_period: usize, lips_period: usize) -> PyResult<Self> {
Ok(Self {
inner: wc::GatorOscillator::new(jaw_period, teeth_period, lips_period)
.map_err(map_err)?,
})
}
fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult<Option<(f64, f64)>> {
let c = extract_candle(candle)?;
Ok(self.inner.update(c).map(|o| (o.upper, o.lower)))
}
/// Batch over high/low/close numpy columns. Returns shape `(n, 2)` for
/// `[upper, lower]`.
fn batch<'py>(
&mut self,
py: Python<'py>,
high: PyReadonlyArray1<'py, f64>,
low: PyReadonlyArray1<'py, f64>,
close: PyReadonlyArray1<'py, f64>,
) -> PyResult<Bound<'py, PyArray2<f64>>> {
let h = high
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
let l = low
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
let c = close
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
if h.len() != l.len() || l.len() != c.len() {
return Err(PyValueError::new_err(
"high, low, close must be equal length",
));
}
let n = h.len();
let mut out = vec![f64::NAN; n * 2];
for i in 0..n {
let candle = wc::Candle::new(c[i], h[i], l[i], c[i], 0.0, 0).map_err(map_err)?;
if let Some(o) = self.inner.update(candle) {
out[i * 2] = o.upper;
out[i * 2 + 1] = o.lower;
}
}
Ok(numpy::ndarray::Array2::from_shape_vec((n, 2), out)
.expect("shape consistent")
.into_pyarray(py))
}
fn reset(&mut self) {
self.inner.reset();
}
fn is_ready(&self) -> bool {
self.inner.is_ready()
}
fn warmup_period(&self) -> usize {
self.inner.warmup_period()
}
}
// ============================== KasePermissionStochastic ==============================
#[pyclass(
name = "KasePermissionStochastic",
module = "wickra._wickra",
skip_from_py_object
)]
#[derive(Clone)]
struct PyKasePermissionStochastic {
inner: wc::KasePermissionStochastic,
}
#[pymethods]
impl PyKasePermissionStochastic {
#[new]
#[pyo3(signature = (length=9, smooth=3))]
fn new(length: usize, smooth: usize) -> PyResult<Self> {
Ok(Self {
inner: wc::KasePermissionStochastic::new(length, smooth).map_err(map_err)?,
})
}
fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult<Option<(f64, f64)>> {
let c = extract_candle(candle)?;
Ok(self.inner.update(c).map(|o| (o.fast, o.slow)))
}
/// Batch over high/low/close numpy columns. Returns shape `(n, 2)` for
/// `[fast, slow]`.
fn batch<'py>(
&mut self,
py: Python<'py>,
high: PyReadonlyArray1<'py, f64>,
low: PyReadonlyArray1<'py, f64>,
close: PyReadonlyArray1<'py, f64>,
) -> PyResult<Bound<'py, PyArray2<f64>>> {
let h = high
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
let l = low
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
let c = close
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
if h.len() != l.len() || l.len() != c.len() {
return Err(PyValueError::new_err(
"high, low, close must be equal length",
));
}
let n = h.len();
let mut out = vec![f64::NAN; n * 2];
for i in 0..n {
let candle = wc::Candle::new(c[i], h[i], l[i], c[i], 0.0, 0).map_err(map_err)?;
if let Some(o) = self.inner.update(candle) {
out[i * 2] = o.fast;
out[i * 2 + 1] = o.slow;
}
}
Ok(numpy::ndarray::Array2::from_shape_vec((n, 2), out)
.expect("shape consistent")
.into_pyarray(py))
}
fn reset(&mut self) {
self.inner.reset();
}
fn is_ready(&self) -> bool {
self.inner.is_ready()
}
fn warmup_period(&self) -> usize {
self.inner.warmup_period()
}
}
// ============================== TsfOscillator ==============================
#[pyclass(name = "TsfOscillator", module = "wickra._wickra", skip_from_py_object)]
#[derive(Clone)]
struct PyTsfOscillator {
inner: wc::TsfOscillator,
}
#[pymethods]
impl PyTsfOscillator {
#[new]
#[pyo3(signature = (period=14))]
fn new(period: usize) -> PyResult<Self> {
Ok(Self {
inner: wc::TsfOscillator::new(period).map_err(map_err)?,
})
}
fn update(&mut self, value: f64) -> Option<f64> {
self.inner.update(value)
}
fn batch<'py>(
&mut self,
py: Python<'py>,
prices: PyReadonlyArray1<'py, f64>,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
let s = prices
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
Ok(flatten(self.inner.batch(s)).into_pyarray(py))
}
#[getter]
fn period(&self) -> usize {
self.inner.period()
}
fn reset(&mut self) {
self.inner.reset();
}
fn is_ready(&self) -> bool {
self.inner.is_ready()
}
fn warmup_period(&self) -> usize {
self.inner.warmup_period()
}
fn __repr__(&self) -> String {
format!("TsfOscillator(period={})", self.inner.period())
}
}
// ============================== MacdHistogram ==============================
#[pyclass(name = "MacdHistogram", module = "wickra._wickra", skip_from_py_object)]
#[derive(Clone)]
struct PyMacdHistogram {
inner: wc::MacdHistogram,
}
#[pymethods]
impl PyMacdHistogram {
#[new]
#[pyo3(signature = (fast=12, slow=26, signal=9))]
fn new(fast: usize, slow: usize, signal: usize) -> PyResult<Self> {
Ok(Self {
inner: wc::MacdHistogram::new(fast, slow, signal).map_err(map_err)?,
})
}
fn update(&mut self, value: f64) -> Option<f64> {
self.inner.update(value)
}
fn batch<'py>(
&mut self,
py: Python<'py>,
prices: PyReadonlyArray1<'py, f64>,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
let s = prices
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
Ok(flatten(self.inner.batch(s)).into_pyarray(py))
}
fn reset(&mut self) {
self.inner.reset();
}
fn is_ready(&self) -> bool {
self.inner.is_ready()
}
fn warmup_period(&self) -> usize {
self.inner.warmup_period()
}
fn __repr__(&self) -> String {
let (fast, slow, signal) = self.inner.periods();
format!("MacdHistogram(fast={fast}, slow={slow}, signal={signal})")
}
}
// ============================== PpoHistogram ==============================
#[pyclass(name = "PpoHistogram", module = "wickra._wickra", skip_from_py_object)]
#[derive(Clone)]
struct PyPpoHistogram {
inner: wc::PpoHistogram,
}
#[pymethods]
impl PyPpoHistogram {
#[new]
#[pyo3(signature = (fast=12, slow=26, signal=9))]
fn new(fast: usize, slow: usize, signal: usize) -> PyResult<Self> {
Ok(Self {
inner: wc::PpoHistogram::new(fast, slow, signal).map_err(map_err)?,
})
}
fn update(&mut self, value: f64) -> Option<f64> {
self.inner.update(value)
}
fn batch<'py>(
&mut self,
py: Python<'py>,
prices: PyReadonlyArray1<'py, f64>,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
let s = prices
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
Ok(flatten(self.inner.batch(s)).into_pyarray(py))
}
fn reset(&mut self) {
self.inner.reset();
}
fn is_ready(&self) -> bool {
self.inner.is_ready()
}
fn warmup_period(&self) -> usize {
self.inner.warmup_period()
}
fn __repr__(&self) -> String {
let (fast, slow, signal) = self.inner.periods();
format!("PpoHistogram(fast={fast}, slow={slow}, signal={signal})")
}
}
// ============================== Stochastic ==============================
#[pyclass(name = "IMI", module = "wickra._wickra", skip_from_py_object)]
@@ -20986,5 +21553,15 @@ fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<PyRsx>()?;
m.add_class::<PyDynamicMomentumIndex>()?;
m.add_class::<PyStochasticCci>()?;
m.add_class::<PyTtmTrend>()?;
m.add_class::<PyTrendStrengthIndex>()?;
m.add_class::<PyQstick>()?;
m.add_class::<PyPolarizedFractalEfficiency>()?;
m.add_class::<PyWavePm>()?;
m.add_class::<PyGatorOscillator>()?;
m.add_class::<PyKasePermissionStochastic>()?;
m.add_class::<PyTsfOscillator>()?;
m.add_class::<PyMacdHistogram>()?;
m.add_class::<PyPpoHistogram>()?;
Ok(())
}
@@ -45,6 +45,12 @@ def ohlcv() -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
# --- Scalar (f64 -> f64) indicators ---------------------------------------
SCALAR = [
(ta.PpoHistogram, (3, 6, 3)),
(ta.MacdHistogram, (3, 6, 3)),
(ta.TsfOscillator, (3,)),
(ta.WAVE_PM, (32, 3)),
(ta.POLARIZED_FRACTAL_EFFICIENCY, (10, 5)),
(ta.TREND_STRENGTH_INDEX, (20,)),
(ta.DerivativeOscillator, (14, 5, 3, 9)),
(ta.RMI, (14, 5)),
(ta.DynamicMomentumIndex, (14,)),
@@ -355,6 +361,7 @@ def test_relative_strength_streaming_matches_batch():
# 6-tuple candle; the batch helper takes only the columns it needs.
CANDLE_SCALAR = {
"TTM_TREND": (lambda: ta.TTM_TREND(6), lambda ind, h, l, c, v: ind.batch(h, l, c)),
"StochasticCCI": (lambda: ta.StochasticCCI(14), lambda ind, h, l, c, v: ind.batch(h, l, c)),
# Per-bar OHLC transforms (open matters). The streaming harness feeds
# open == close, so batch passes the close column in for open to match.
@@ -892,6 +899,16 @@ def test_candle_scalar_streaming_matches_batch(name, ohlcv):
# --- Candle-input, multi-output indicators --------------------------------
MULTI = {
"KasePermissionStochastic": (
lambda: ta.KasePermissionStochastic(9, 3),
lambda ind, h, l, c, v: ind.batch(h, l, c),
2,
),
"GatorOscillator": (
lambda: ta.GatorOscillator(13, 8, 5),
lambda ind, h, l, c, v: ind.batch(h, l, c),
2,
),
"ElderRay": (
lambda: ta.ElderRay(13),
lambda ind, h, l, c, v: ind.batch(h, l, c),
@@ -2779,6 +2796,100 @@ def test_imi_reference():
assert math.isnan(out[1])
assert out[2] == pytest.approx(75.0)
def test_qstick_reference():
q = ta.Qstick(3)
open_ = np.array([10.0, 10.0, 10.0])
close = np.array([11.0, 11.0, 11.0])
out = q.batch(open_, close)
# Each body is close - open = 1; SMA(3) of [1, 1, 1] = 1.
assert math.isnan(out[0])
assert math.isnan(out[1])
assert out[2] == pytest.approx(1.0)
def test_ttm_trend_reference():
t = ta.TTM_TREND(3)
high = np.array([13.0, 13.0, 13.0])
low = np.array([9.0, 9.0, 9.0])
close = np.array([12.0, 12.0, 12.0])
out = t.batch(high, low, close)
# Median (13 + 9) / 2 = 11; close 12 is above the SMA(3) reference -> +1.
assert math.isnan(out[0])
assert out[2] == pytest.approx(1.0)
def test_trend_strength_index_reference():
tsi = ta.TREND_STRENGTH_INDEX(10)
closes = np.arange(10, dtype=float)
out = tsi.batch(closes)
# A clean ramp is a perfect uptrend -> signed r^2 = +1.
assert math.isclose(out[-1], 1.0, abs_tol=1e-9)
def test_polarized_fractal_efficiency_reference():
pfe = ta.POLARIZED_FRACTAL_EFFICIENCY(5, 3)
closes = np.arange(20, dtype=float)
out = pfe.batch(closes)
# On a straight ramp the path equals the diagonal -> efficiency 1 -> +100.
assert math.isclose(out[-1], 100.0, abs_tol=1e-9)
def test_wave_pm_reference():
wpm = ta.WAVE_PM(10, 3)
closes = np.arange(60, dtype=float) * 5.0
out = wpm.batch(closes)
# Constant-slope ramp: momentum equals its energy -> 100 * (1 - e^-0.5).
baseline = 100.0 * (1.0 - math.exp(-0.5))
assert math.isclose(out[-1], baseline, abs_tol=1e-9)
def test_gator_oscillator_reference():
g = ta.GatorOscillator(13, 8, 5)
n = 40
high = np.full(n, 11.0)
low = np.full(n, 9.0)
close = np.full(n, 10.0)
out = g.batch(high, low, close)
# Constant median collapses all three Alligator lines -> both bars zero.
assert out[-1][0] == pytest.approx(0.0)
assert out[-1][1] == pytest.approx(0.0)
def test_kase_permission_stochastic_reference():
k = ta.KasePermissionStochastic(4, 2)
n = 20
flat = np.full(n, 10.0)
out = k.batch(flat, flat, flat)
# HH == LL -> raw %K defaults to the neutral 50 -> both lines at 50.
assert out[-1][0] == pytest.approx(50.0)
assert out[-1][1] == pytest.approx(50.0)
def test_tsf_oscillator_reference():
t = ta.TsfOscillator(3)
assert t.update(1.0) is None
assert t.update(2.0) is None
assert t.update(9.0) == pytest.approx(-33.33333333333333)
def test_macd_histogram_reference():
# On a constant-slope ramp the MACD line is flat once seeded, so the
# signal EMA catches up and the histogram collapses to 0.
t = ta.MacdHistogram(3, 6, 3)
for i in range(7):
assert t.update(100.0 + i * 2.0) is None
assert t.update(100.0 + 7 * 2.0) == pytest.approx(0.0, abs=1e-9)
def test_ppo_histogram_reference():
# PPO divides the EMA gap by the slow EMA, so on the same ramp the ratio
# keeps drifting and the histogram stays non-zero.
t = ta.PpoHistogram(3, 6, 3)
for i in range(7):
assert t.update(100.0 + i * 2.0) is None
assert t.update(100.0 + 7 * 2.0) == pytest.approx(-0.052098, abs=1e-6)
# --- Lifecycle ------------------------------------------------------------
+225
View File
@@ -80,6 +80,14 @@ wasm_scalar_indicator!(WasmTrima, "TRIMA", wc::Trima, period: usize);
wasm_scalar_indicator!(WasmZlema, "ZLEMA", wc::Zlema, period: usize);
wasm_scalar_indicator!(WasmT3, "T3", wc::T3, period: usize, v: f64);
wasm_scalar_indicator!(WasmAlma, "ALMA", wc::Alma, period: usize, offset: f64, sigma: f64);
wasm_scalar_indicator!(
WasmPolarizedFractalEfficiency,
"POLARIZED_FRACTAL_EFFICIENCY",
wc::PolarizedFractalEfficiency,
period: usize,
smoothing: usize
);
wasm_scalar_indicator!(WasmWavePm, "WAVE_PM", wc::WavePm, length: usize, smoothing: usize);
wasm_scalar_indicator!(WasmMcGinleyDynamic, "McGinleyDynamic", wc::McGinleyDynamic, period: usize);
wasm_scalar_indicator!(WasmFrama, "FRAMA", wc::Frama, period: usize);
wasm_scalar_indicator!(WasmVidya, "VIDYA", wc::Vidya, period: usize, cmo_period: usize);
@@ -2255,6 +2263,219 @@ impl WasmElderRay {
}
}
#[wasm_bindgen(js_name = TTM_TREND)]
pub struct WasmTtmTrend {
inner: wc::TtmTrend,
}
#[wasm_bindgen(js_class = TTM_TREND)]
impl WasmTtmTrend {
#[wasm_bindgen(constructor)]
pub fn new(period: usize) -> Result<WasmTtmTrend, JsError> {
Ok(Self {
inner: wc::TtmTrend::new(period).map_err(map_err)?,
})
}
pub fn update(&mut self, high: f64, low: f64, close: f64) -> Result<Option<f64>, JsError> {
let c = make_candle(high, low, close, 0.0)?;
Ok(self.inner.update(c))
}
pub fn batch(
&mut self,
high: &[f64],
low: &[f64],
close: &[f64],
) -> Result<Float64Array, JsError> {
if high.len() != low.len() || low.len() != close.len() {
return Err(JsError::new("high, low, close must be equal length"));
}
let mut out = Vec::with_capacity(high.len());
for i in 0..high.len() {
let c = make_candle(high[i], low[i], close[i], 0.0)?;
out.push(self.inner.update(c).unwrap_or(f64::NAN));
}
Ok(Float64Array::from(out.as_slice()))
}
pub fn reset(&mut self) {
self.inner.reset();
}
}
#[wasm_bindgen(js_name = Qstick)]
pub struct WasmQstick {
inner: wc::Qstick,
}
#[wasm_bindgen(js_class = Qstick)]
impl WasmQstick {
#[wasm_bindgen(constructor)]
pub fn new(period: usize) -> Result<WasmQstick, JsError> {
Ok(Self {
inner: wc::Qstick::new(period).map_err(map_err)?,
})
}
/// Batch over open/close arrays; `NaN` during warmup.
pub fn batch(&mut self, open: &[f64], close: &[f64]) -> Result<Float64Array, JsError> {
let n = open.len();
if close.len() != n {
return Err(JsError::new("open, close must be equal length"));
}
let mut out = vec![f64::NAN; n];
for i in 0..n {
let hi = open[i].max(close[i]);
let lo = open[i].min(close[i]);
let c = make_candle_ohlc(open[i], hi, lo, close[i])?;
if let Some(v) = self.inner.update(c) {
out[i] = v;
}
}
Ok(Float64Array::from(out.as_slice()))
}
pub fn reset(&mut self) {
self.inner.reset();
}
/// Streaming update over one candle's open and close.
pub fn update(&mut self, open: f64, close: f64) -> Result<Option<f64>, JsError> {
let hi = open.max(close);
let lo = open.min(close);
let c = make_candle_ohlc(open, hi, lo, close)?;
Ok(self.inner.update(c))
}
#[wasm_bindgen(js_name = isReady)]
pub fn is_ready(&self) -> bool {
self.inner.is_ready()
}
#[wasm_bindgen(js_name = warmupPeriod)]
pub fn warmup_period(&self) -> usize {
self.inner.warmup_period()
}
}
#[wasm_bindgen(js_name = GatorOscillator)]
pub struct WasmGatorOscillator {
inner: wc::GatorOscillator,
}
#[wasm_bindgen(js_class = GatorOscillator)]
impl WasmGatorOscillator {
#[wasm_bindgen(constructor)]
pub fn new(
jaw_period: usize,
teeth_period: usize,
lips_period: usize,
) -> Result<WasmGatorOscillator, JsError> {
Ok(Self {
inner: wc::GatorOscillator::new(jaw_period, teeth_period, lips_period)
.map_err(map_err)?,
})
}
/// Returns `[upper0, lower0, upper1, lower1, ...]`, length `2 * n`.
pub fn batch(
&mut self,
high: &[f64],
low: &[f64],
close: &[f64],
) -> Result<Float64Array, JsError> {
let n = high.len();
if low.len() != n || close.len() != n {
return Err(JsError::new("high, low, close must be equal length"));
}
let mut out = vec![f64::NAN; n * 2];
for i in 0..n {
let c = make_candle(high[i], low[i], close[i], 0.0)?;
if let Some(o) = self.inner.update(c) {
out[i * 2] = o.upper;
out[i * 2 + 1] = o.lower;
}
}
Ok(Float64Array::from(out.as_slice()))
}
pub fn reset(&mut self) {
self.inner.reset();
}
/// Streaming update. Returns `{ upper, lower }` once warm, else `null`.
pub fn update(&mut self, high: f64, low: f64, close: f64) -> Result<JsValue, JsError> {
let c = make_candle(high, low, close, 0.0)?;
Ok(match self.inner.update(c) {
Some(o) => {
let obj = Object::new();
Reflect::set(&obj, &"upper".into(), &o.upper.into()).ok();
Reflect::set(&obj, &"lower".into(), &o.lower.into()).ok();
obj.into()
}
None => JsValue::NULL,
})
}
#[wasm_bindgen(js_name = isReady)]
pub fn is_ready(&self) -> bool {
self.inner.is_ready()
}
#[wasm_bindgen(js_name = warmupPeriod)]
pub fn warmup_period(&self) -> usize {
self.inner.warmup_period()
}
}
#[wasm_bindgen(js_name = KasePermissionStochastic)]
pub struct WasmKasePermissionStochastic {
inner: wc::KasePermissionStochastic,
}
#[wasm_bindgen(js_class = KasePermissionStochastic)]
impl WasmKasePermissionStochastic {
#[wasm_bindgen(constructor)]
pub fn new(length: usize, smooth: usize) -> Result<WasmKasePermissionStochastic, JsError> {
Ok(Self {
inner: wc::KasePermissionStochastic::new(length, smooth).map_err(map_err)?,
})
}
/// Returns `[fast0, slow0, fast1, slow1, ...]`, length `2 * n`.
pub fn batch(
&mut self,
high: &[f64],
low: &[f64],
close: &[f64],
) -> Result<Float64Array, JsError> {
let n = high.len();
if low.len() != n || close.len() != n {
return Err(JsError::new("high, low, close must be equal length"));
}
let mut out = vec![f64::NAN; n * 2];
for i in 0..n {
let c = make_candle(high[i], low[i], close[i], 0.0)?;
if let Some(o) = self.inner.update(c) {
out[i * 2] = o.fast;
out[i * 2 + 1] = o.slow;
}
}
Ok(Float64Array::from(out.as_slice()))
}
pub fn reset(&mut self) {
self.inner.reset();
}
/// Streaming update. Returns `{ fast, slow }` once warm, else `null`.
pub fn update(&mut self, high: f64, low: f64, close: f64) -> Result<JsValue, JsError> {
let c = make_candle(high, low, close, 0.0)?;
Ok(match self.inner.update(c) {
Some(o) => {
let obj = Object::new();
Reflect::set(&obj, &"fast".into(), &o.fast.into()).ok();
Reflect::set(&obj, &"slow".into(), &o.slow.into()).ok();
obj.into()
}
None => JsValue::NULL,
})
}
#[wasm_bindgen(js_name = isReady)]
pub fn is_ready(&self) -> bool {
self.inner.is_ready()
}
#[wasm_bindgen(js_name = warmupPeriod)]
pub fn warmup_period(&self) -> usize {
self.inner.warmup_period()
}
}
#[wasm_bindgen(js_name = Stochastic)]
pub struct WasmStoch {
inner: wc::Stochastic,
@@ -10431,6 +10652,10 @@ wasm_scalar_indicator!(WasmRsx, "RSX", wc::Rsx, period: usize);
wasm_scalar_indicator!(WasmDynamicMomentumIndex, "DynamicMomentumIndex", wc::DynamicMomentumIndex, period: usize);
wasm_scalar_indicator!(WasmRmi, "RMI", wc::Rmi, period: usize, momentum: usize);
wasm_scalar_indicator!(WasmDerivativeOscillator, "DerivativeOscillator", wc::DerivativeOscillator, rsi_period: usize, smooth1: usize, smooth2: usize, signal_period: usize);
wasm_scalar_indicator!(WasmTrendStrengthIndex, "TREND_STRENGTH_INDEX", wc::TrendStrengthIndex, period: usize);
wasm_scalar_indicator!(WasmTsfOscillator, "TsfOscillator", wc::TsfOscillator, period: usize);
wasm_scalar_indicator!(WasmMacdHistogram, "MacdHistogram", wc::MacdHistogram, fast: usize, slow: usize, signal: usize);
wasm_scalar_indicator!(WasmPpoHistogram, "PpoHistogram", wc::PpoHistogram, fast: usize, slow: usize, signal: usize);
// --- DrawdownDuration: u32 output, no constructor args ---
@@ -0,0 +1,205 @@
//! Bill Williams' Gator Oscillator (derived from the Alligator).
use crate::error::Result;
use crate::indicators::alligator::Alligator;
use crate::ohlcv::Candle;
use crate::traits::Indicator;
/// Gator Oscillator output: the two histogram bars drawn above and below the
/// zero line.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct GatorOscillatorOutput {
/// Upper histogram `|jaw - teeth|`, always `>= 0`.
pub upper: f64,
/// Lower histogram `-|teeth - lips|`, always `<= 0`.
pub lower: f64,
}
/// Bill Williams' Gator Oscillator: a convergence/divergence view of the
/// [`Alligator`] lines. The upper bar is the absolute gap between Jaw and
/// Teeth; the lower bar is the negated absolute gap between Teeth and Lips.
///
/// ```text
/// upper = |jaw - teeth|
/// lower = -|teeth - lips |
/// ```
///
/// Widening bars mean the Alligator's mouth is opening (a trending market);
/// shrinking bars mean it is closing (consolidation). Warmup matches the
/// underlying Alligator — the first value appears once the slowest line (Jaw)
/// has warmed up.
///
/// Reference: Bill Williams, *Trading Chaos*, 1995.
///
/// # Example
///
/// ```
/// use wickra_core::{Candle, GatorOscillator, Indicator};
///
/// let mut indicator = GatorOscillator::classic();
/// let mut last = None;
/// for i in 0..40 {
/// let base = 100.0 + f64::from(i);
/// let candle =
/// Candle::new(base, base + 1.0, base - 1.0, base, 1.0, i64::from(i)).unwrap();
/// last = indicator.update(candle);
/// }
/// assert!(last.is_some());
/// ```
#[derive(Debug, Clone)]
pub struct GatorOscillator {
alligator: Alligator,
}
impl GatorOscillator {
/// Construct a Gator Oscillator from explicit Alligator periods
/// `(jaw, teeth, lips)`.
///
/// # Errors
///
/// Returns [`Error::PeriodZero`](crate::error::Error::PeriodZero) if any period is zero.
pub fn new(jaw_period: usize, teeth_period: usize, lips_period: usize) -> Result<Self> {
Ok(Self {
alligator: Alligator::new(jaw_period, teeth_period, lips_period)?,
})
}
/// Bill Williams' classic parameters: `(jaw = 13, teeth = 8, lips = 5)`.
pub fn classic() -> Self {
Self {
alligator: Alligator::classic(),
}
}
/// Configured `(jaw_period, teeth_period, lips_period)`.
pub const fn periods(&self) -> (usize, usize, usize) {
self.alligator.periods()
}
}
impl Indicator for GatorOscillator {
type Input = Candle;
type Output = GatorOscillatorOutput;
fn update(&mut self, candle: Candle) -> Option<GatorOscillatorOutput> {
let lines = self.alligator.update(candle)?;
Some(GatorOscillatorOutput {
upper: (lines.jaw - lines.teeth).abs(),
lower: -(lines.teeth - lines.lips).abs(),
})
}
fn reset(&mut self) {
self.alligator.reset();
}
fn warmup_period(&self) -> usize {
self.alligator.warmup_period()
}
fn is_ready(&self) -> bool {
self.alligator.is_ready()
}
fn name(&self) -> &'static str {
"GatorOscillator"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::Error;
use crate::traits::BatchExt;
use approx::assert_relative_eq;
fn candle(high: f64, low: f64, ts: i64) -> Candle {
let close = f64::midpoint(high, low);
Candle::new(close, high, low, close, 1.0, ts).unwrap()
}
#[test]
fn rejects_zero_period() {
assert!(matches!(
GatorOscillator::new(0, 8, 5),
Err(Error::PeriodZero)
));
assert!(matches!(
GatorOscillator::new(13, 0, 5),
Err(Error::PeriodZero)
));
assert!(matches!(
GatorOscillator::new(13, 8, 0),
Err(Error::PeriodZero)
));
}
#[test]
fn accessors_and_metadata() {
let g = GatorOscillator::classic();
assert_eq!(g.periods(), (13, 8, 5));
assert_eq!(g.warmup_period(), 13);
assert_eq!(g.name(), "GatorOscillator");
assert!(!g.is_ready());
}
#[test]
fn constant_series_collapses_both_bars() {
// All three Alligator lines equal the constant median -> zero spread.
let mut g = GatorOscillator::classic();
let candles: Vec<Candle> = (0..40).map(|i| candle(11.0, 9.0, i)).collect();
let out = g.batch(&candles);
let last = out.last().unwrap().unwrap();
assert_relative_eq!(last.upper, 0.0, epsilon = 1e-12);
assert_relative_eq!(last.lower, 0.0, epsilon = 1e-12);
}
#[test]
fn trending_series_opens_the_mouth() {
// On a clean trend the lines separate -> upper > 0, lower < 0.
let mut g = GatorOscillator::classic();
let candles: Vec<Candle> = (0_i64..80)
.map(|i| candle(10.0 + i as f64, 9.0 + i as f64, i))
.collect();
let last = g.batch(&candles).last().unwrap().unwrap();
assert!(last.upper > 0.0, "upper {} should be positive", last.upper);
assert!(last.lower < 0.0, "lower {} should be negative", last.lower);
}
#[test]
fn warmup_emits_first_value_at_longest_period() {
let mut g = GatorOscillator::new(5, 3, 2).unwrap();
let candles: Vec<Candle> = (0..6).map(|i| candle(11.0, 9.0, i)).collect();
let out = g.batch(&candles);
for v in out.iter().take(4) {
assert!(v.is_none());
}
assert!(out[4].is_some());
}
#[test]
fn reset_clears_state() {
let mut g = GatorOscillator::classic();
let candles: Vec<Candle> = (0..40).map(|i| candle(11.0, 9.0, i)).collect();
g.batch(&candles);
assert!(g.is_ready());
g.reset();
assert!(!g.is_ready());
}
#[test]
fn batch_equals_streaming() {
let candles: Vec<Candle> = (0..80_i64)
.map(|i| {
let base = 100.0 + (i as f64 * 0.2).sin() * 5.0;
candle(base + 1.0, base - 1.0, i)
})
.collect();
let mut a = GatorOscillator::classic();
let mut b = GatorOscillator::classic();
assert_eq!(
a.batch(&candles),
candles.iter().map(|c| b.update(*c)).collect::<Vec<_>>()
);
}
}
@@ -0,0 +1,234 @@
//! Kase Permission Stochastic — a double-smoothed stochastic used as a
//! trade-permission filter.
use std::collections::VecDeque;
use crate::error::{Error, Result};
use crate::indicators::ema::Ema;
use crate::ohlcv::Candle;
use crate::traits::Indicator;
/// Kase Permission Stochastic output: a fast and a slow line.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct KasePermissionStochasticOutput {
/// Fast line: EMA of the raw `%K` over the smoothing period.
pub fast: f64,
/// Slow line: EMA of the fast line over the smoothing period.
pub slow: f64,
}
/// Cynthia Kase's Permission Stochastic: a stochastic oscillator smoothed twice,
/// whose fast/slow relationship grants or denies "permission" to trade in the
/// direction of a higher-timeframe signal.
///
/// ```text
/// raw%K = 100 * (close - LL) / (HH - LL) over `length` (50 when HH == LL)
/// fast = EMA(raw%K, smooth)
/// slow = EMA(fast, smooth)
/// ```
///
/// The raw stochastic is the usual `%K`, then an EMA produces the *fast* line
/// and a second EMA of that produces the *slow* line. Kase uses the pair as a
/// gate: a fast line above the slow line (and rising) gives permission for
/// longs, the reverse for shorts. When the lookback window is perfectly flat
/// (`HH == LL`), the raw stochastic is undefined and defaults to the neutral
/// `50`.
///
/// Reference: Cynthia Kase, *Trading with the Odds*, 1996.
///
/// # Example
///
/// ```
/// use wickra_core::{Candle, Indicator, KasePermissionStochastic};
///
/// let mut indicator = KasePermissionStochastic::new(9, 3).unwrap();
/// let mut last = None;
/// for i in 0..40 {
/// let base = 100.0 + f64::from(i);
/// let candle =
/// Candle::new(base, base + 2.0, base - 2.0, base + 1.0, 1.0, i64::from(i)).unwrap();
/// last = indicator.update(candle);
/// }
/// assert!(last.is_some());
/// ```
#[derive(Debug, Clone)]
pub struct KasePermissionStochastic {
length: usize,
smooth: usize,
window: VecDeque<(f64, f64)>,
fast_ema: Ema,
slow_ema: Ema,
}
impl KasePermissionStochastic {
/// Construct with the stochastic `length` and the EMA `smooth` period
/// applied twice.
///
/// # Errors
///
/// Returns [`Error::PeriodZero`] if `length == 0` or `smooth == 0`.
pub fn new(length: usize, smooth: usize) -> Result<Self> {
if length == 0 {
return Err(Error::PeriodZero);
}
Ok(Self {
length,
smooth,
window: VecDeque::with_capacity(length),
fast_ema: Ema::new(smooth)?,
slow_ema: Ema::new(smooth)?,
})
}
/// Cynthia Kase's classic parameters: `length = 9`, `smooth = 3`.
pub fn classic() -> Self {
Self::new(9, 3).expect("classic Kase Permission Stochastic parameters are valid")
}
/// Configured `(length, smooth)`.
pub const fn periods(&self) -> (usize, usize) {
(self.length, self.smooth)
}
}
impl Indicator for KasePermissionStochastic {
type Input = Candle;
type Output = KasePermissionStochasticOutput;
fn update(&mut self, candle: Candle) -> Option<KasePermissionStochasticOutput> {
self.window.push_back((candle.high, candle.low));
if self.window.len() > self.length {
self.window.pop_front();
}
if self.window.len() < self.length {
return None;
}
let highest = self.window.iter().map(|w| w.0).fold(f64::MIN, f64::max);
let lowest = self.window.iter().map(|w| w.1).fold(f64::MAX, f64::min);
let raw_k = if highest > lowest {
100.0 * (candle.close - lowest) / (highest - lowest)
} else {
50.0
};
let fast = self.fast_ema.update(raw_k)?;
let slow = self.slow_ema.update(fast)?;
Some(KasePermissionStochasticOutput { fast, slow })
}
fn reset(&mut self) {
self.window.clear();
self.fast_ema.reset();
self.slow_ema.reset();
}
fn warmup_period(&self) -> usize {
// raw%K ready after `length` bars; each EMA seeds over `smooth` values.
self.length + 2 * self.smooth - 2
}
fn is_ready(&self) -> bool {
self.slow_ema.is_ready()
}
fn name(&self) -> &'static str {
"KasePermissionStochastic"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
use approx::assert_relative_eq;
fn candle(high: f64, low: f64, close: f64, ts: i64) -> Candle {
Candle::new(f64::midpoint(high, low), high, low, close, 1.0, ts).unwrap()
}
#[test]
fn rejects_zero_period() {
assert!(matches!(
KasePermissionStochastic::new(0, 3),
Err(Error::PeriodZero)
));
assert!(matches!(
KasePermissionStochastic::new(9, 0),
Err(Error::PeriodZero)
));
}
#[test]
fn accessors_and_metadata() {
let k = KasePermissionStochastic::classic();
assert_eq!(k.periods(), (9, 3));
// 9 + 2*3 - 2 = 13.
assert_eq!(k.warmup_period(), 13);
assert_eq!(k.name(), "KasePermissionStochastic");
assert!(!k.is_ready());
}
#[test]
fn warmup_emits_at_expected_bar() {
let mut k = KasePermissionStochastic::new(3, 2).unwrap();
// warmup = 3 + 2*2 - 2 = 5 -> first value at input 5 (index 4).
let candles: Vec<Candle> = (0..8).map(|i| candle(11.0, 9.0, 10.5, i)).collect();
let out = k.batch(&candles);
assert!(out[3].is_none());
assert!(out[4].is_some());
}
#[test]
fn top_of_range_is_high() {
// Close pinned at the top of a rising range -> raw%K near 100, both
// smoothed lines high.
let mut k = KasePermissionStochastic::new(5, 3).unwrap();
let candles: Vec<Candle> = (0_i64..40)
.map(|i| {
let base = 100.0 + i as f64;
candle(base + 2.0, base - 2.0, base + 2.0, i)
})
.collect();
let last = k.batch(&candles).last().unwrap().unwrap();
assert!(last.fast > 80.0, "fast {} should be high", last.fast);
assert!(last.slow > 80.0, "slow {} should be high", last.slow);
}
#[test]
fn flat_window_defaults_to_neutral() {
// Constant high/low/close -> HH == LL -> raw%K defaults to 50, so both
// EMAs converge to 50.
let mut k = KasePermissionStochastic::new(4, 2).unwrap();
let candles: Vec<Candle> = (0..20).map(|i| candle(10.0, 10.0, 10.0, i)).collect();
let last = k.batch(&candles).last().unwrap().unwrap();
assert_relative_eq!(last.fast, 50.0, epsilon = 1e-9);
assert_relative_eq!(last.slow, 50.0, epsilon = 1e-9);
}
#[test]
fn reset_clears_state() {
let mut k = KasePermissionStochastic::classic();
let candles: Vec<Candle> = (0..40).map(|i| candle(11.0, 9.0, 10.5, i)).collect();
k.batch(&candles);
assert!(k.is_ready());
k.reset();
assert!(!k.is_ready());
}
#[test]
fn batch_equals_streaming() {
let candles: Vec<Candle> = (0..80_i64)
.map(|i| {
let base = 100.0 + (i as f64 * 0.2).sin() * 5.0;
candle(base + 2.0, base - 2.0, base + (i as f64 * 0.3).cos(), i)
})
.collect();
let mut a = KasePermissionStochastic::classic();
let mut b = KasePermissionStochastic::classic();
assert_eq!(
a.batch(&candles),
candles.iter().map(|c| b.update(*c)).collect::<Vec<_>>()
);
}
}
@@ -0,0 +1,184 @@
//! MACD Histogram (standalone).
use crate::error::Result;
use crate::indicators::macd::MacdIndicator;
use crate::traits::Indicator;
/// MACD Histogram — the `macd signal` bar of [`MacdIndicator`] as a
/// standalone scalar indicator.
///
/// ```text
/// macd = EMA(fast) EMA(slow)
/// signal = EMA(macd, signal)
/// histogram = macd signal
/// ```
///
/// The histogram is the most actively traded part of MACD: it crosses zero
/// exactly when the MACD line crosses its signal, and its slope measures
/// whether that momentum is accelerating or fading. This wrapper exposes just
/// that series for pipelines that want a plain `f64` stream rather than the
/// full [`MacdOutput`](crate::MacdOutput); for the line and signal alongside
/// it, use [`MacdIndicator`](crate::MacdIndicator) directly.
///
/// Standard parameters are `fast = 12`, `slow = 26`, `signal = 9`, so the
/// first value lands after `slow + signal 1` inputs — exactly when
/// [`MacdIndicator`] emits its first full output.
///
/// # Example
///
/// ```
/// use wickra_core::{Indicator, MacdHistogram};
///
/// let mut indicator = MacdHistogram::new(12, 26, 9).unwrap();
/// let mut last = None;
/// for i in 0..80 {
/// last = indicator.update(100.0 + f64::from(i));
/// }
/// assert!(last.is_some());
/// ```
#[derive(Debug, Clone)]
pub struct MacdHistogram {
macd: MacdIndicator,
}
impl MacdHistogram {
/// Construct a MACD histogram with the given periods.
///
/// # Errors
///
/// Returns [`Error::PeriodZero`] if any period is zero, and
/// [`Error::InvalidPeriod`] if `fast >= slow`.
pub fn new(fast: usize, slow: usize, signal: usize) -> Result<Self> {
Ok(Self {
macd: MacdIndicator::new(fast, slow, signal)?,
})
}
/// Default `(12, 26, 9)` configuration, matching every classical chart package.
pub fn classic() -> Self {
Self::new(12, 26, 9).expect("classic MACD periods are valid")
}
/// Configured periods as `(fast, slow, signal)`.
pub const fn periods(&self) -> (usize, usize, usize) {
self.macd.periods()
}
}
impl Indicator for MacdHistogram {
type Input = f64;
type Output = f64;
fn update(&mut self, input: f64) -> Option<f64> {
self.macd.update(input).map(|out| out.histogram)
}
fn reset(&mut self) {
self.macd.reset();
}
fn warmup_period(&self) -> usize {
self.macd.warmup_period()
}
fn is_ready(&self) -> bool {
self.macd.is_ready()
}
fn name(&self) -> &'static str {
"MacdHistogram"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::Error;
use crate::traits::BatchExt;
use approx::assert_relative_eq;
#[test]
fn rejects_invalid_periods() {
assert!(matches!(
MacdHistogram::new(0, 26, 9),
Err(Error::PeriodZero)
));
assert!(matches!(
MacdHistogram::new(12, 26, 0),
Err(Error::PeriodZero)
));
assert!(matches!(
MacdHistogram::new(26, 12, 9),
Err(Error::InvalidPeriod { .. })
));
}
#[test]
fn accessors_and_metadata() {
let osc = MacdHistogram::classic();
assert_eq!(osc.periods(), (12, 26, 9));
assert_eq!(osc.name(), "MacdHistogram");
assert_eq!(osc.warmup_period(), 26 + 9 - 1);
assert!(!osc.is_ready());
}
#[test]
fn equals_macd_histogram_field() {
// The standalone series must be exactly MacdIndicator's histogram bar.
let prices: Vec<f64> = (1..=120)
.map(|i| 100.0 + (f64::from(i) * 0.25).sin() * 8.0)
.collect();
let hist = MacdHistogram::classic().batch(&prices);
let full = MacdIndicator::classic().batch(&prices);
assert_eq!(hist.len(), full.len());
for (h, m) in hist.iter().zip(full.iter()) {
assert_eq!(h.is_some(), m.is_some());
if let (Some(h), Some(m)) = (h, m) {
assert_relative_eq!(*h, m.histogram, epsilon = 1e-12);
}
}
}
#[test]
fn warmup_emits_first_value_at_warmup_period() {
let mut osc = MacdHistogram::new(3, 6, 3).unwrap();
let warmup = osc.warmup_period();
assert_eq!(warmup, 6 + 3 - 1);
for i in 1..warmup {
assert!(osc.update(100.0 + i as f64).is_none());
}
assert!(osc.update(100.0 + warmup as f64).is_some());
assert!(osc.is_ready());
}
#[test]
fn constant_series_converges_to_zero() {
let mut osc = MacdHistogram::classic();
let out = osc.batch(&[100.0_f64; 200]);
let last = out.iter().rev().flatten().next().expect("emits a value");
assert_relative_eq!(*last, 0.0, epsilon = 1e-9);
}
#[test]
fn batch_equals_streaming() {
let prices: Vec<f64> = (1..=100)
.map(|i| (f64::from(i) * 0.4).cos() * 10.0)
.collect();
let mut a = MacdHistogram::classic();
let mut b = MacdHistogram::classic();
assert_eq!(
a.batch(&prices),
prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
);
}
#[test]
fn reset_clears_state() {
let mut osc = MacdHistogram::classic();
osc.batch(&(1..=80).map(f64::from).collect::<Vec<_>>());
assert!(osc.is_ready());
osc.reset();
assert!(!osc.is_ready());
assert_eq!(osc.update(1.0), None);
}
}
+31 -1
View File
@@ -145,6 +145,7 @@ mod gain_loss_ratio;
mod gap_side_by_side_white;
mod garman_klass;
mod gartley;
mod gator_oscillator;
mod generalized_dema;
mod geometric_ma;
mod golden_pocket;
@@ -187,6 +188,7 @@ mod jump_indicator;
mod kagi_bars;
mod kalman_hedge_ratio;
mod kama;
mod kase_permission_stochastic;
mod kelly_criterion;
mod keltner;
mod kicking;
@@ -212,6 +214,7 @@ mod ma_envelope;
mod macd;
mod macd_ext;
mod macd_fix;
mod macd_histogram;
mod mama;
mod market_facilitation_index;
mod marubozu;
@@ -266,11 +269,14 @@ mod plus_di;
mod plus_dm;
mod pmo;
mod point_and_figure_bars;
mod polarized_fractal_efficiency;
mod ppo;
mod ppo_histogram;
mod profit_factor;
mod psar;
mod pvi;
mod qqe;
mod qstick;
mod quoted_spread;
mod r_squared;
mod realized_spread;
@@ -368,6 +374,7 @@ mod time_of_day_return_profile;
mod tpo_profile;
mod trade_imbalance;
mod trend_label;
mod trend_strength_index;
mod treynor_ratio;
mod triangle;
mod trima;
@@ -376,9 +383,11 @@ mod triple_top_bottom;
mod trix;
mod true_range;
mod tsf;
mod tsf_oscillator;
mod tsi;
mod tsv;
mod ttm_squeeze;
mod ttm_trend;
mod turn_of_month;
mod tweezer;
mod two_crows;
@@ -406,6 +415,7 @@ mod vwap;
mod vwap_stddev_bands;
mod vwma;
mod vzo;
mod wave_pm;
mod wave_trend;
mod wedge;
mod weighted_close;
@@ -558,6 +568,7 @@ pub use gain_loss_ratio::GainLossRatio;
pub use gap_side_by_side_white::GapSideBySideWhite;
pub use garman_klass::GarmanKlassVolatility;
pub use gartley::Gartley;
pub use gator_oscillator::{GatorOscillator, GatorOscillatorOutput};
pub use generalized_dema::GeneralizedDema;
pub use geometric_ma::GeometricMa;
pub use golden_pocket::{GoldenPocket, GoldenPocketOutput};
@@ -600,6 +611,7 @@ pub use jump_indicator::JumpIndicator;
pub use kagi_bars::{KagiBar, KagiBars};
pub use kalman_hedge_ratio::{KalmanHedgeRatio, KalmanHedgeRatioOutput};
pub use kama::Kama;
pub use kase_permission_stochastic::{KasePermissionStochastic, KasePermissionStochasticOutput};
pub use kelly_criterion::KellyCriterion;
pub use keltner::{Keltner, KeltnerOutput};
pub use kicking::Kicking;
@@ -625,6 +637,7 @@ pub use ma_envelope::{MaEnvelope, MaEnvelopeOutput};
pub use macd::{MacdIndicator, MacdOutput};
pub use macd_ext::{MaType, MacdExt};
pub use macd_fix::MacdFix;
pub use macd_histogram::MacdHistogram;
pub use mama::{Mama, MamaOutput};
pub use market_facilitation_index::MarketFacilitationIndex;
pub use marubozu::Marubozu;
@@ -679,11 +692,14 @@ pub use plus_di::PlusDi;
pub use plus_dm::PlusDm;
pub use pmo::Pmo;
pub use point_and_figure_bars::{PnfColumn, PointAndFigureBars};
pub use polarized_fractal_efficiency::PolarizedFractalEfficiency;
pub use ppo::Ppo;
pub use ppo_histogram::PpoHistogram;
pub use profit_factor::ProfitFactor;
pub use psar::Psar;
pub use pvi::Pvi;
pub use qqe::{Qqe, QqeOutput};
pub use qstick::Qstick;
pub use quoted_spread::QuotedSpread;
pub use r_squared::RSquared;
pub use realized_spread::RealizedSpread;
@@ -781,6 +797,7 @@ pub use time_of_day_return_profile::{TimeOfDayReturnProfile, TimeOfDayReturnProf
pub use tpo_profile::{TpoProfile, TpoProfileOutput};
pub use trade_imbalance::TradeImbalance;
pub use trend_label::TrendLabel;
pub use trend_strength_index::TrendStrengthIndex;
pub use treynor_ratio::TreynorRatio;
pub use triangle::Triangle;
pub use trima::Trima;
@@ -789,9 +806,11 @@ pub use triple_top_bottom::TripleTopBottom;
pub use trix::Trix;
pub use true_range::TrueRange;
pub use tsf::Tsf;
pub use tsf_oscillator::TsfOscillator;
pub use tsi::Tsi;
pub use tsv::Tsv;
pub use ttm_squeeze::{TtmSqueeze, TtmSqueezeOutput};
pub use ttm_trend::TtmTrend;
pub use turn_of_month::TurnOfMonth;
pub use tweezer::Tweezer;
pub use two_crows::TwoCrows;
@@ -819,6 +838,7 @@ pub use vwap::{RollingVwap, Vwap};
pub use vwap_stddev_bands::{VwapStdDevBands, VwapStdDevBandsOutput};
pub use vwma::Vwma;
pub use vzo::Vzo;
pub use wave_pm::WavePm;
pub use wave_trend::{WaveTrend, WaveTrendOutput};
pub use wedge::Wedge;
pub use weighted_close::WeightedClose;
@@ -936,6 +956,13 @@ pub const FAMILIES: &[(&str, &[&str])] = &[
"MinusDi",
"Dx",
"TrendLabel",
"TtmTrend",
"TrendStrengthIndex",
"Qstick",
"PolarizedFractalEfficiency",
"WavePm",
"GatorOscillator",
"KasePermissionStochastic",
],
),
(
@@ -952,6 +979,9 @@ pub const FAMILIES: &[(&str, &[&str])] = &[
"ZeroLagMacd",
"ElderImpulse",
"Stc",
"TsfOscillator",
"MacdHistogram",
"PpoHistogram",
],
),
(
@@ -1393,6 +1423,6 @@ mod family_tests {
// the actual indicator count is the early-warning signal that an
// indicator was added without being assigned a family.
let total: usize = FAMILIES.iter().map(|(_, ns)| ns.len()).sum();
assert_eq!(total, 413, "FAMILIES total drifted from indicator count");
assert_eq!(total, 423, "FAMILIES total drifted from indicator count");
}
}
@@ -0,0 +1,243 @@
//! Polarized Fractal Efficiency (PFE).
use std::collections::VecDeque;
use crate::error::{Error, Result};
use crate::indicators::ema::Ema;
use crate::traits::Indicator;
/// Polarized Fractal Efficiency: how efficiently price travelled over the last
/// `period` bars, signed by direction and smoothed by an EMA.
///
/// ```text
/// straight = sqrt((C_t - C_{t-n})^2 + n^2) (direct distance over n bars)
/// path = Σ_{i=1..n} sqrt((C_{t-i+1} - C_{t-i})^2 + 1) (sum of single-bar steps)
/// raw = 100 * sign(C_t - C_{t-n}) * straight / path
/// PFE = EMA(raw, smoothing)
/// ```
///
/// The ratio `straight / path` is the fractal efficiency: it is `1` when price
/// moved in a perfectly straight line and falls toward `0` as the path becomes
/// jagged. Polarizing it by the sign of the net move pushes the reading to
/// `+100` for an efficient up-move and `-100` for an efficient down-move, with
/// choppy markets oscillating near zero. Because each single-bar step and the
/// `n`-bar diagonal both carry the bar count on the x-axis (`+1` and `+n^2`),
/// the path length is always `>= n`, so the denominator can never be zero.
///
/// Reference: Hans Hannula, *Stocks & Commodities*, 1994.
///
/// # Example
///
/// ```
/// use wickra_core::{Indicator, PolarizedFractalEfficiency};
///
/// let mut indicator = PolarizedFractalEfficiency::new(10, 5).unwrap();
/// let mut last = None;
/// for i in 0..40 {
/// last = indicator.update(100.0 + f64::from(i));
/// }
/// assert!(last.is_some());
/// ```
#[derive(Debug, Clone)]
pub struct PolarizedFractalEfficiency {
period: usize,
smoothing: usize,
closes: VecDeque<f64>,
prev_close: Option<f64>,
segments: VecDeque<f64>,
segment_sum: f64,
ema: Ema,
}
impl PolarizedFractalEfficiency {
/// Construct a PFE with the fractal lookback `period` and the EMA
/// `smoothing` period.
///
/// # Errors
///
/// Returns [`Error::PeriodZero`] if `period == 0` or `smoothing == 0`.
pub fn new(period: usize, smoothing: usize) -> Result<Self> {
if period == 0 {
return Err(Error::PeriodZero);
}
Ok(Self {
period,
smoothing,
closes: VecDeque::with_capacity(period + 1),
prev_close: None,
segments: VecDeque::with_capacity(period),
segment_sum: 0.0,
ema: Ema::new(smoothing)?,
})
}
/// Configured `(period, smoothing)`.
pub const fn periods(&self) -> (usize, usize) {
(self.period, self.smoothing)
}
}
impl Indicator for PolarizedFractalEfficiency {
type Input = f64;
type Output = f64;
fn update(&mut self, close: f64) -> Option<f64> {
if let Some(prev) = self.prev_close {
let diff = close - prev;
let segment = diff.mul_add(diff, 1.0).sqrt();
self.segment_sum += segment;
self.segments.push_back(segment);
if self.segments.len() > self.period {
self.segment_sum -= self.segments.pop_front().unwrap_or(0.0);
}
}
self.prev_close = Some(close);
self.closes.push_back(close);
if self.closes.len() > self.period + 1 {
self.closes.pop_front();
}
if self.closes.len() <= self.period {
return None;
}
let oldest = *self.closes.front().unwrap_or(&close);
let net = close - oldest;
let direction = if net > 0.0 {
1.0
} else if net < 0.0 {
-1.0
} else {
0.0
};
let span = self.period as f64;
let straight = net.mul_add(net, span * span).sqrt();
let raw = 100.0 * direction * straight / self.segment_sum;
self.ema.update(raw)
}
fn reset(&mut self) {
self.closes.clear();
self.prev_close = None;
self.segments.clear();
self.segment_sum = 0.0;
self.ema.reset();
}
fn warmup_period(&self) -> usize {
self.period + self.smoothing
}
fn is_ready(&self) -> bool {
self.ema.is_ready()
}
fn name(&self) -> &'static str {
"PolarizedFractalEfficiency"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
use approx::assert_relative_eq;
#[test]
fn rejects_zero_period() {
assert!(matches!(
PolarizedFractalEfficiency::new(0, 5),
Err(Error::PeriodZero)
));
assert!(matches!(
PolarizedFractalEfficiency::new(10, 0),
Err(Error::PeriodZero)
));
}
#[test]
fn accessors_and_metadata() {
let pfe = PolarizedFractalEfficiency::new(10, 5).unwrap();
assert_eq!(pfe.periods(), (10, 5));
assert_eq!(pfe.warmup_period(), 15);
assert_eq!(pfe.name(), "PolarizedFractalEfficiency");
assert!(!pfe.is_ready());
}
#[test]
fn warmup_emits_after_period_plus_smoothing() {
let mut pfe = PolarizedFractalEfficiency::new(4, 2).unwrap();
// raw needs period+1 = 5 closes; EMA(2) needs 2 raws -> first value at
// input 6 (index 5).
let inputs: Vec<f64> = (0..10).map(f64::from).collect();
let out = pfe.batch(&inputs);
assert!(out[4].is_none());
assert!(out[5].is_some());
}
#[test]
fn perfect_uptrend_is_strongly_positive() {
// A straight ramp: every step is +1, the diagonal is maximally
// efficient, so PFE saturates near +100.
let mut pfe = PolarizedFractalEfficiency::new(5, 3).unwrap();
let inputs: Vec<f64> = (0..30).map(f64::from).collect();
let last = pfe.batch(&inputs).last().unwrap().unwrap();
assert!(last > 99.0, "pfe {last} should be near +100");
}
#[test]
fn perfect_downtrend_is_strongly_negative() {
let mut pfe = PolarizedFractalEfficiency::new(5, 3).unwrap();
let inputs: Vec<f64> = (0..30).map(|i| -f64::from(i)).collect();
let last = pfe.batch(&inputs).last().unwrap().unwrap();
assert!(last < -99.0, "pfe {last} should be near -100");
}
#[test]
fn flat_market_returns_zero() {
// No net move over the window -> direction 0 -> raw 0 -> PFE 0.
let mut pfe = PolarizedFractalEfficiency::new(5, 3).unwrap();
let inputs = [10.0; 20];
let last = pfe.batch(&inputs).last().unwrap().unwrap();
assert_relative_eq!(last, 0.0, epsilon = 1e-12);
}
#[test]
fn choppy_market_is_inefficient() {
// A sawtooth whip: the net move is tiny relative to the jagged path, so
// efficiency stays well below the +-100 saturation of a clean trend.
let mut pfe = PolarizedFractalEfficiency::new(5, 3).unwrap();
let inputs: Vec<f64> = (0..40)
.map(|i| if i % 2 == 0 { 100.0 } else { 102.0 })
.collect();
let last = pfe.batch(&inputs).last().unwrap().unwrap();
assert!(
last.abs() < 60.0,
"choppy pfe {last} should be far from +-100"
);
}
#[test]
fn reset_clears_state() {
let mut pfe = PolarizedFractalEfficiency::new(5, 3).unwrap();
let inputs: Vec<f64> = (0..30).map(f64::from).collect();
pfe.batch(&inputs);
assert!(pfe.is_ready());
pfe.reset();
assert!(!pfe.is_ready());
assert_eq!(pfe.periods(), (5, 3));
}
#[test]
fn batch_equals_streaming() {
let inputs: Vec<f64> = (0..80)
.map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 5.0)
.collect();
let mut a = PolarizedFractalEfficiency::new(10, 5).unwrap();
let mut b = PolarizedFractalEfficiency::new(10, 5).unwrap();
assert_eq!(
a.batch(&inputs),
inputs.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
);
}
}
@@ -0,0 +1,230 @@
//! Percentage Price Oscillator Histogram.
use crate::error::{Error, Result};
use crate::indicators::ema::Ema;
use crate::indicators::ppo::Ppo;
use crate::traits::Indicator;
/// PPO Histogram — the `ppo signal` bar of the Percentage Price Oscillator.
///
/// ```text
/// ppo = 100 · (EMA_fast EMA_slow) / EMA_slow
/// signal = EMA(ppo, signal_period)
/// histogram = ppo signal
/// ```
///
/// [`Ppo`](crate::Ppo) itself only emits the percentage line; this indicator
/// adds the classic 9-period signal EMA on top and reports the resulting
/// zero-centered histogram. Because PPO is scale-free (the EMA gap is divided
/// by the slow EMA), the histogram is **comparable across instruments** — a
/// PPO histogram of `0.4` means the same relative momentum on any asset, unlike
/// the price-unit [`MacdHistogram`](crate::MacdHistogram).
///
/// With Appel's defaults `fast = 12`, `slow = 26`, `signal = 9`, the first
/// value lands after `slow + signal 1` inputs — the point at which the slow
/// EMA and then the signal EMA are both seeded.
///
/// # Example
///
/// ```
/// use wickra_core::{Indicator, PpoHistogram};
///
/// let mut indicator = PpoHistogram::new(12, 26, 9).unwrap();
/// let mut last = None;
/// for i in 0..80 {
/// last = indicator.update(100.0 + f64::from(i));
/// }
/// assert!(last.is_some());
/// ```
#[derive(Debug, Clone)]
pub struct PpoHistogram {
ppo: Ppo,
signal_ema: Ema,
signal_period: usize,
current: Option<f64>,
}
impl PpoHistogram {
/// Construct a PPO histogram with the `fast`/`slow` EMA periods and the
/// `signal` EMA period.
///
/// # Errors
///
/// Returns [`Error::PeriodZero`] if any period is `0`, or
/// [`Error::InvalidPeriod`] if `fast >= slow`.
pub fn new(fast: usize, slow: usize, signal: usize) -> Result<Self> {
if signal == 0 {
return Err(Error::PeriodZero);
}
Ok(Self {
ppo: Ppo::new(fast, slow)?,
signal_ema: Ema::new(signal)?,
signal_period: signal,
current: None,
})
}
/// Default `(12, 26, 9)` configuration.
pub fn classic() -> Self {
Self::new(12, 26, 9).expect("classic PPO periods are valid")
}
/// Configured periods as `(fast, slow, signal)`.
pub const fn periods(&self) -> (usize, usize, usize) {
let (fast, slow) = self.ppo.periods();
(fast, slow, self.signal_period)
}
/// Current value if available.
pub const fn value(&self) -> Option<f64> {
self.current
}
}
impl Indicator for PpoHistogram {
type Input = f64;
type Output = f64;
fn update(&mut self, input: f64) -> Option<f64> {
// Guard before touching either stage so a non-finite input never
// advances the signal EMA on a stale, re-fed PPO value.
if !input.is_finite() {
return self.current;
}
let ppo = self.ppo.update(input)?;
let signal = self.signal_ema.update(ppo)?;
let histogram = ppo - signal;
self.current = Some(histogram);
Some(histogram)
}
fn reset(&mut self) {
self.ppo.reset();
self.signal_ema.reset();
self.current = None;
}
fn warmup_period(&self) -> usize {
// Slow EMA seeds the PPO, then the signal EMA needs `signal 1` more.
self.ppo.warmup_period() + self.signal_period - 1
}
fn is_ready(&self) -> bool {
self.current.is_some()
}
fn name(&self) -> &'static str {
"PpoHistogram"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
use approx::assert_relative_eq;
#[test]
fn rejects_invalid_periods() {
assert!(matches!(
PpoHistogram::new(0, 26, 9),
Err(Error::PeriodZero)
));
assert!(matches!(
PpoHistogram::new(12, 0, 9),
Err(Error::PeriodZero)
));
assert!(matches!(
PpoHistogram::new(12, 26, 0),
Err(Error::PeriodZero)
));
assert!(matches!(
PpoHistogram::new(26, 12, 9),
Err(Error::InvalidPeriod { .. })
));
}
#[test]
fn accessors_and_metadata() {
let osc = PpoHistogram::classic();
assert_eq!(osc.periods(), (12, 26, 9));
assert_eq!(osc.name(), "PpoHistogram");
assert_eq!(osc.warmup_period(), 26 + 9 - 1);
assert_eq!(osc.value(), None);
assert!(!osc.is_ready());
}
#[test]
fn equals_ppo_minus_signal_ema() {
// The histogram must equal PPO minus an EMA(signal) composed by hand.
let prices: Vec<f64> = (1..=120)
.map(|i| 100.0 + (f64::from(i) * 0.2).sin() * 6.0)
.collect();
let got = PpoHistogram::new(12, 26, 9).unwrap().batch(&prices);
let mut ppo = Ppo::new(12, 26).unwrap();
let mut sig = Ema::new(9).unwrap();
let mut expected = Vec::with_capacity(prices.len());
for p in &prices {
let out = ppo
.update(*p)
.and_then(|line| sig.update(line).map(|signal| line - signal));
expected.push(out);
}
assert_eq!(got, expected);
}
#[test]
fn warmup_emits_first_value_at_warmup_period() {
let mut osc = PpoHistogram::new(3, 6, 3).unwrap();
let warmup = osc.warmup_period();
assert_eq!(warmup, 6 + 3 - 1);
for i in 1..warmup {
assert!(osc.update(100.0 + i as f64).is_none());
}
assert!(osc.update(100.0 + warmup as f64).is_some());
assert!(osc.is_ready());
}
#[test]
fn constant_series_converges_to_zero() {
let mut osc = PpoHistogram::classic();
let out = osc.batch(&[100.0_f64; 200]);
let last = out.iter().rev().flatten().next().expect("emits a value");
assert_relative_eq!(*last, 0.0, epsilon = 1e-9);
}
#[test]
fn ignores_non_finite_input() {
let mut osc = PpoHistogram::new(3, 6, 3).unwrap();
let out = osc.batch(&(1..=40).map(f64::from).collect::<Vec<_>>());
let before = *out.last().unwrap();
assert!(before.is_some());
assert_eq!(osc.update(f64::NAN), before);
assert_eq!(osc.update(f64::INFINITY), before);
assert_eq!(osc.value(), before);
}
#[test]
fn batch_equals_streaming() {
let prices: Vec<f64> = (1..=100)
.map(|i| 100.0 + (f64::from(i) * 0.4).cos() * 10.0)
.collect();
let mut a = PpoHistogram::classic();
let mut b = PpoHistogram::classic();
assert_eq!(
a.batch(&prices),
prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
);
}
#[test]
fn reset_clears_state() {
let mut osc = PpoHistogram::classic();
osc.batch(&(1..=80).map(f64::from).collect::<Vec<_>>());
assert!(osc.is_ready());
osc.reset();
assert!(!osc.is_ready());
assert_eq!(osc.update(1.0), None);
}
}
+168
View File
@@ -0,0 +1,168 @@
//! Qstick — Tushar Chande's measure of buying vs. selling pressure.
use crate::error::Result;
use crate::indicators::sma::Sma;
use crate::ohlcv::Candle;
use crate::traits::Indicator;
/// Qstick: the simple moving average of the body `close - open` over `period`
/// bars.
///
/// Positive values indicate a run of bars that closed above their open (net
/// buying pressure); negative values indicate net selling pressure. A zero
/// crossing is read as a shift in short-term sentiment.
///
/// ```text
/// Qstick = SMA(close - open, period)
/// ```
///
/// Reference: Tushar Chande, *The New Technical Trader*, 1994.
///
/// # Example
///
/// ```
/// use wickra_core::{Candle, Indicator, Qstick};
///
/// let mut indicator = Qstick::new(5).unwrap();
/// let mut last = None;
/// for i in 0..20 {
/// let base = 100.0 + f64::from(i);
/// let candle =
/// Candle::new(base, base + 2.0, base - 1.0, base + 1.0, 1.0, i64::from(i)).unwrap();
/// last = indicator.update(candle);
/// }
/// assert!(last.is_some());
/// ```
#[derive(Debug, Clone)]
pub struct Qstick {
period: usize,
sma: Sma,
}
impl Qstick {
/// Construct a Qstick with the given averaging period.
///
/// # Errors
///
/// Returns [`Error::PeriodZero`](crate::error::Error::PeriodZero) if `period == 0`.
pub fn new(period: usize) -> Result<Self> {
Ok(Self {
period,
sma: Sma::new(period)?,
})
}
/// Configured averaging period.
pub const fn period(&self) -> usize {
self.period
}
}
impl Indicator for Qstick {
type Input = Candle;
type Output = f64;
fn update(&mut self, candle: Candle) -> Option<f64> {
self.sma.update(candle.close - candle.open)
}
fn reset(&mut self) {
self.sma.reset();
}
fn warmup_period(&self) -> usize {
self.period
}
fn is_ready(&self) -> bool {
self.sma.is_ready()
}
fn name(&self) -> &'static str {
"Qstick"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::Error;
use crate::traits::BatchExt;
use approx::assert_relative_eq;
fn candle(open: f64, close: f64, ts: i64) -> Candle {
let high = open.max(close) + 1.0;
let low = open.min(close) - 1.0;
Candle::new(open, high, low, close, 1.0, ts).unwrap()
}
#[test]
fn rejects_zero_period() {
assert!(matches!(Qstick::new(0), Err(Error::PeriodZero)));
}
#[test]
fn accessors_and_metadata() {
let q = Qstick::new(5).unwrap();
assert_eq!(q.period(), 5);
assert_eq!(q.warmup_period(), 5);
assert_eq!(q.name(), "Qstick");
assert!(!q.is_ready());
}
#[test]
fn warmup_emits_first_value_at_period() {
let mut q = Qstick::new(3).unwrap();
let candles: Vec<Candle> = (0..3).map(|i| candle(10.0, 11.0, i)).collect();
let out = q.batch(&candles);
assert!(out[0].is_none());
assert!(out[1].is_none());
assert!(out[2].is_some());
}
#[test]
fn constant_bodies_yield_the_body() {
// Every bar closes 1.5 above its open -> Qstick converges to 1.5.
let mut q = Qstick::new(4).unwrap();
let candles: Vec<Candle> = (0..10).map(|i| candle(10.0, 11.5, i)).collect();
let out = q.batch(&candles);
assert_relative_eq!(out.last().unwrap().unwrap(), 1.5, epsilon = 1e-12);
}
#[test]
fn selling_pressure_is_negative() {
let mut q = Qstick::new(3).unwrap();
let candles: Vec<Candle> = (0..6).map(|i| candle(11.0, 10.0, i)).collect();
let last = q.batch(&candles).last().unwrap().unwrap();
assert!(last < 0.0, "qstick {last} should be negative");
}
#[test]
fn reset_clears_state() {
let mut q = Qstick::new(3).unwrap();
let candles: Vec<Candle> = (0..6).map(|i| candle(10.0, 11.0, i)).collect();
q.batch(&candles);
assert!(q.is_ready());
q.reset();
assert!(!q.is_ready());
}
#[test]
fn batch_equals_streaming() {
let candles: Vec<Candle> = (0..40_i64)
.map(|i| {
candle(
100.0 + (i as f64 * 0.3).sin(),
100.0 + (i as f64 * 0.4).cos(),
i,
)
})
.collect();
let mut a = Qstick::new(7).unwrap();
let mut b = Qstick::new(7).unwrap();
assert_eq!(
a.batch(&candles),
candles.iter().map(|c| b.update(*c)).collect::<Vec<_>>()
);
}
}
@@ -0,0 +1,218 @@
//! Trend Strength Index — the signed coefficient of determination of a linear
//! regression of price against time.
use std::collections::VecDeque;
use crate::error::{Error, Result};
use crate::traits::Indicator;
/// Trend Strength Index: fits an ordinary-least-squares line to the last
/// `period` prices against their bar index and reports the coefficient of
/// determination `r^2`, signed by the slope of the fit.
///
/// ```text
/// regress y = close on x = 0..period-1
/// r^2 = (n·Σxy Σx·Σy)^2 / [ (n·Σx² (Σx)²)(n·Σy² (Σy)²) ]
/// TSI = sign(slope) · r^2 (slope sign = sign of n·Σxy Σx·Σy)
/// ```
///
/// `r^2` in `[0, 1]` measures how well a straight line explains the price over
/// the window — how *trendy* the segment is, regardless of direction. Carrying
/// the slope sign turns it into a directional reading in `[-1, 1]`: values near
/// `+1` are a strong, clean uptrend; near `-1` a strong downtrend; near `0` a
/// flat or noisy market with no linear structure. A window of constant prices
/// (zero variance in `y`) has no defined trend and returns `0`.
///
/// # Example
///
/// ```
/// use wickra_core::{Indicator, TrendStrengthIndex};
///
/// let mut indicator = TrendStrengthIndex::new(20).unwrap();
/// let mut last = None;
/// for i in 0..40 {
/// last = indicator.update(100.0 + f64::from(i));
/// }
/// // A clean ramp is a perfect uptrend -> r^2 = 1.
/// assert!((last.unwrap() - 1.0).abs() < 1e-9);
/// ```
#[derive(Debug, Clone)]
pub struct TrendStrengthIndex {
period: usize,
buf: VecDeque<f64>,
}
impl TrendStrengthIndex {
/// Construct a Trend Strength Index over the given window.
///
/// # Errors
///
/// Returns [`Error::PeriodZero`] if `period == 0`, or [`Error::InvalidPeriod`]
/// if `period == 1` (a regression needs at least two points).
pub fn new(period: usize) -> Result<Self> {
if period == 0 {
return Err(Error::PeriodZero);
}
if period == 1 {
return Err(Error::InvalidPeriod {
message: "period must be >= 2 for a regression",
});
}
Ok(Self {
period,
buf: VecDeque::with_capacity(period),
})
}
/// Configured window length.
pub const fn period(&self) -> usize {
self.period
}
}
impl Indicator for TrendStrengthIndex {
type Input = f64;
type Output = f64;
fn update(&mut self, price: f64) -> Option<f64> {
self.buf.push_back(price);
if self.buf.len() > self.period {
self.buf.pop_front();
}
if self.buf.len() < self.period {
return None;
}
let count = self.period as f64;
let mut sum_x = 0.0;
let mut sum_xx = 0.0;
let mut sum_y = 0.0;
let mut sum_yy = 0.0;
let mut sum_xy = 0.0;
for (idx, &price) in self.buf.iter().enumerate() {
let x = idx as f64;
sum_x += x;
sum_xx += x * x;
sum_y += price;
sum_yy += price * price;
sum_xy += x * price;
}
let cov = count.mul_add(sum_xy, -(sum_x * sum_y));
let var_x = count.mul_add(sum_xx, -(sum_x * sum_x));
let var_y = count.mul_add(sum_yy, -(sum_y * sum_y));
if var_y <= 0.0 {
return Some(0.0);
}
let r2 = (cov * cov) / (var_x * var_y);
Some(if cov >= 0.0 { r2 } else { -r2 })
}
fn reset(&mut self) {
self.buf.clear();
}
fn warmup_period(&self) -> usize {
self.period
}
fn is_ready(&self) -> bool {
self.buf.len() >= self.period
}
fn name(&self) -> &'static str {
"TrendStrengthIndex"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
use approx::assert_relative_eq;
#[test]
fn rejects_invalid_period() {
assert!(matches!(TrendStrengthIndex::new(0), Err(Error::PeriodZero)));
assert!(matches!(
TrendStrengthIndex::new(1),
Err(Error::InvalidPeriod { .. })
));
}
#[test]
fn accessors_and_metadata() {
let tsi = TrendStrengthIndex::new(20).unwrap();
assert_eq!(tsi.period(), 20);
assert_eq!(tsi.warmup_period(), 20);
assert_eq!(tsi.name(), "TrendStrengthIndex");
assert!(!tsi.is_ready());
}
#[test]
fn warmup_emits_at_period() {
let mut tsi = TrendStrengthIndex::new(4).unwrap();
let inputs: Vec<f64> = (0..6).map(f64::from).collect();
let out = tsi.batch(&inputs);
assert!(out[2].is_none());
assert!(out[3].is_some());
}
#[test]
fn perfect_uptrend_is_plus_one() {
let mut tsi = TrendStrengthIndex::new(10).unwrap();
let inputs: Vec<f64> = (0..10).map(f64::from).collect();
let last = tsi.batch(&inputs).last().unwrap().unwrap();
assert_relative_eq!(last, 1.0, epsilon = 1e-9);
}
#[test]
fn perfect_downtrend_is_minus_one() {
let mut tsi = TrendStrengthIndex::new(10).unwrap();
let inputs: Vec<f64> = (0..10).map(|i| 100.0 - f64::from(i)).collect();
let last = tsi.batch(&inputs).last().unwrap().unwrap();
assert_relative_eq!(last, -1.0, epsilon = 1e-9);
}
#[test]
fn flat_market_returns_zero() {
let mut tsi = TrendStrengthIndex::new(8).unwrap();
let inputs = [42.0; 12];
let last = tsi.batch(&inputs).last().unwrap().unwrap();
assert_relative_eq!(last, 0.0, epsilon = 1e-12);
}
#[test]
fn noisy_trend_is_between() {
// An upward drift with noise: positive but not a perfect fit.
let mut tsi = TrendStrengthIndex::new(12).unwrap();
let inputs: Vec<f64> = (0..12)
.map(|i| f64::from(i) + if i % 2 == 0 { 0.0 } else { 3.0 })
.collect();
let last = tsi.batch(&inputs).last().unwrap().unwrap();
assert!(last > 0.0 && last < 1.0, "tsi {last} should be in (0, 1)");
}
#[test]
fn reset_clears_state() {
let mut tsi = TrendStrengthIndex::new(10).unwrap();
let inputs: Vec<f64> = (0..10).map(f64::from).collect();
tsi.batch(&inputs);
assert!(tsi.is_ready());
tsi.reset();
assert!(!tsi.is_ready());
}
#[test]
fn batch_equals_streaming() {
let inputs: Vec<f64> = (0..80)
.map(|i| 100.0 + (f64::from(i) * 0.2).sin() * 5.0)
.collect();
let mut a = TrendStrengthIndex::new(15).unwrap();
let mut b = TrendStrengthIndex::new(15).unwrap();
assert_eq!(
a.batch(&inputs),
inputs.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
);
}
}
@@ -0,0 +1,206 @@
//! Time Series Forecast Oscillator (TSF Oscillator).
use crate::error::{Error, Result};
use crate::indicators::tsf::Tsf;
use crate::traits::Indicator;
/// Time Series Forecast Oscillator — the percentage gap between the close and
/// the **one-bar-ahead** time-series forecast of the close.
///
/// ```text
/// TSFOsc_t = 100 · (close_t TSF(close, period)_t) / close_t
/// ```
///
/// where [`Tsf`](crate::Tsf) projects the rolling least-squares line one bar
/// past the window (`a + b·period`). It is the close-relative companion to
/// [`Cfo`](crate::Cfo), which measures the same percentage gap against the
/// regression value at the *current* bar (`a + b·(period 1)`). Because `TSF`
/// advances one bar further than `LinearRegression`, the two differ by exactly
/// the slope term `100·b/close`: on a trending series `TSFOsc` reads more
/// negative in an uptrend (the forecast has already stepped above price) and
/// more positive in a downtrend.
///
/// Positive readings mean the close sits *above* its forward forecast (price
/// has overshot the projected trend); negative readings mean it sits below.
/// Wraps the existing `Tsf` so the warmup matches.
///
/// # Example
///
/// ```
/// use wickra_core::{Indicator, TsfOscillator};
///
/// let mut indicator = TsfOscillator::new(14).unwrap();
/// let mut last = None;
/// for i in 0..40 {
/// last = indicator.update(100.0 + f64::from(i));
/// }
/// assert!(last.is_some());
/// ```
#[derive(Debug, Clone)]
pub struct TsfOscillator {
period: usize,
tsf: Tsf,
current: Option<f64>,
}
impl TsfOscillator {
/// Construct a new TSF oscillator over `period` inputs.
///
/// # Errors
/// Returns [`Error::InvalidPeriod`] if `period < 2` — a regression line is
/// undefined for fewer than two points.
pub fn new(period: usize) -> Result<Self> {
if period < 2 {
return Err(Error::InvalidPeriod {
message: "TSF oscillator needs period >= 2",
});
}
Ok(Self {
period,
tsf: Tsf::new(period)?,
current: None,
})
}
/// Configured period.
pub const fn period(&self) -> usize {
self.period
}
}
impl Indicator for TsfOscillator {
type Input = f64;
type Output = f64;
fn update(&mut self, input: f64) -> Option<f64> {
let forecast = self.tsf.update(input)?;
// Hold the previous value if the close is zero — the percentage form
// is undefined and a return of inf would propagate badly.
if input == 0.0 {
return self.current;
}
let value = 100.0 * (input - forecast) / input;
self.current = Some(value);
Some(value)
}
fn reset(&mut self) {
self.tsf.reset();
self.current = None;
}
fn warmup_period(&self) -> usize {
self.period
}
fn is_ready(&self) -> bool {
self.current.is_some()
}
fn name(&self) -> &'static str {
"TsfOscillator"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
use approx::assert_relative_eq;
#[test]
fn rejects_short_period() {
assert!(matches!(
TsfOscillator::new(1),
Err(Error::InvalidPeriod { .. })
));
assert!(matches!(
TsfOscillator::new(0),
Err(Error::InvalidPeriod { .. })
));
}
#[test]
fn accessors_and_metadata() {
let osc = TsfOscillator::new(14).unwrap();
assert_eq!(osc.period(), 14);
assert_eq!(osc.warmup_period(), 14);
assert_eq!(osc.name(), "TsfOscillator");
assert!(!osc.is_ready());
}
#[test]
fn reference_value() {
// period 3 over [1, 2, 9]: fit y = 0 + 4x, one-bar-ahead TSF at x = 3
// is 12. With close = 9, TSFOsc = 100·(9 12)/9 = 33.3333…%.
let mut osc = TsfOscillator::new(3).unwrap();
let out = osc.batch(&[1.0_f64, 2.0, 9.0]);
assert!(out[0].is_none());
assert!(out[1].is_none());
assert_relative_eq!(out[2].unwrap(), -100.0 / 3.0, epsilon = 1e-9);
assert!(osc.is_ready());
}
#[test]
fn constant_series_yields_zero() {
// On a flat series the regression slope is 0, so the one-bar-ahead TSF
// equals the constant and close forecast is exactly 0.
let mut osc = TsfOscillator::new(5).unwrap();
let out = osc.batch(&[42.0_f64; 30]);
for v in out.iter().skip(4).flatten() {
assert_relative_eq!(*v, 0.0, epsilon = 1e-12);
}
}
#[test]
fn linear_uptrend_reads_negative() {
// Unlike CFO (evaluated at the current bar), the forecast steps one bar
// ahead, so on a rising line the projection sits above the close and the
// oscillator is negative: TSFOsc = 100·slope/close.
let mut osc = TsfOscillator::new(5).unwrap();
let prices: Vec<f64> = (1..=20).map(|i| f64::from(i) * 2.0).collect();
let out = osc.batch(&prices);
for v in out.iter().skip(4).flatten() {
assert!(*v < 0.0, "uptrend forecast overshoots close, got {v}");
}
}
#[test]
fn warmup_emits_first_value_at_period() {
let mut osc = TsfOscillator::new(3).unwrap();
assert_eq!(osc.update(1.0), None);
assert_eq!(osc.update(2.0), None);
assert!(osc.update(3.0).is_some());
}
#[test]
fn batch_equals_streaming() {
let prices: Vec<f64> = (1..=80)
.map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 5.0)
.collect();
let mut a = TsfOscillator::new(14).unwrap();
let mut b = TsfOscillator::new(14).unwrap();
assert_eq!(
a.batch(&prices),
prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
);
}
#[test]
fn reset_clears_state() {
let mut osc = TsfOscillator::new(5).unwrap();
osc.batch(&(1..=20).map(f64::from).collect::<Vec<_>>());
assert!(osc.is_ready());
osc.reset();
assert!(!osc.is_ready());
assert_eq!(osc.update(1.0), None);
}
#[test]
fn zero_close_holds_value() {
let mut osc = TsfOscillator::new(3).unwrap();
osc.batch(&[1.0_f64, 2.0, 3.0]);
let before = osc.current;
assert_eq!(osc.update(0.0), before);
}
}
@@ -0,0 +1,167 @@
//! TTM Trend — John Carter's bar-coloring trend filter.
use crate::error::Result;
use crate::indicators::sma::Sma;
use crate::ohlcv::Candle;
use crate::traits::Indicator;
/// TTM Trend: compares the current close to the simple moving average of the
/// recent median prices `(high + low) / 2`. A close above that reference colors
/// the bar as an uptrend (`+1.0`); a close at or below it as a downtrend
/// (`-1.0`).
///
/// ```text
/// reference = SMA((high + low) / 2, period)
/// TTM Trend = +1 if close > reference
/// -1 otherwise
/// ```
///
/// The classic TTM Trend uses the trailing six bars. The signal is a regime
/// label rather than a level: it stays `None` during warmup and then emits
/// `±1.0` on every bar.
///
/// Reference: John Carter, *Mastering the Trade*, 2005.
///
/// # Example
///
/// ```
/// use wickra_core::{Candle, Indicator, TtmTrend};
///
/// let mut indicator = TtmTrend::new(6).unwrap();
/// let mut last = None;
/// for i in 0..20 {
/// let base = 100.0 + f64::from(i);
/// let candle =
/// Candle::new(base, base + 1.0, base - 1.0, base + 0.5, 1.0, i64::from(i)).unwrap();
/// last = indicator.update(candle);
/// }
/// assert_eq!(last, Some(1.0));
/// ```
#[derive(Debug, Clone)]
pub struct TtmTrend {
period: usize,
sma: Sma,
}
impl TtmTrend {
/// Construct a TTM Trend over the given lookback.
///
/// # Errors
///
/// Returns [`Error::PeriodZero`](crate::error::Error::PeriodZero) if `period == 0`.
pub fn new(period: usize) -> Result<Self> {
Ok(Self {
period,
sma: Sma::new(period)?,
})
}
/// Configured lookback period.
pub const fn period(&self) -> usize {
self.period
}
}
impl Indicator for TtmTrend {
type Input = Candle;
type Output = f64;
fn update(&mut self, candle: Candle) -> Option<f64> {
let median = f64::midpoint(candle.high, candle.low);
let reference = self.sma.update(median)?;
Some(if candle.close > reference { 1.0 } else { -1.0 })
}
fn reset(&mut self) {
self.sma.reset();
}
fn warmup_period(&self) -> usize {
self.period
}
fn is_ready(&self) -> bool {
self.sma.is_ready()
}
fn name(&self) -> &'static str {
"TtmTrend"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::Error;
use crate::traits::BatchExt;
fn candle(high: f64, low: f64, close: f64, ts: i64) -> Candle {
Candle::new(f64::midpoint(high, low), high, low, close, 1.0, ts).unwrap()
}
#[test]
fn rejects_zero_period() {
assert!(matches!(TtmTrend::new(0), Err(Error::PeriodZero)));
}
#[test]
fn accessors_and_metadata() {
let t = TtmTrend::new(6).unwrap();
assert_eq!(t.period(), 6);
assert_eq!(t.warmup_period(), 6);
assert_eq!(t.name(), "TtmTrend");
assert!(!t.is_ready());
}
#[test]
fn warmup_then_emits() {
let mut t = TtmTrend::new(3).unwrap();
let candles: Vec<Candle> = (0..3).map(|i| candle(13.0, 9.0, 12.0, i)).collect();
let out = t.batch(&candles);
assert!(out[0].is_none());
assert!(out[1].is_none());
assert!(out[2].is_some());
}
#[test]
fn close_above_reference_is_uptrend() {
// Close (12) sits above the median reference (13 + 9) / 2 = 11 -> +1.
let mut t = TtmTrend::new(3).unwrap();
let candles: Vec<Candle> = (0..6).map(|i| candle(13.0, 9.0, 12.0, i)).collect();
assert_eq!(t.batch(&candles).last().unwrap().unwrap(), 1.0);
}
#[test]
fn close_at_or_below_reference_is_downtrend() {
// Constant median 10, close equal to the reference -> not strictly above -> -1.
let mut t = TtmTrend::new(3).unwrap();
let candles: Vec<Candle> = (0..6).map(|i| candle(11.0, 9.0, 10.0, i)).collect();
assert_eq!(t.batch(&candles).last().unwrap().unwrap(), -1.0);
}
#[test]
fn reset_clears_state() {
let mut t = TtmTrend::new(3).unwrap();
let candles: Vec<Candle> = (0..6).map(|i| candle(13.0, 9.0, 12.0, i)).collect();
t.batch(&candles);
assert!(t.is_ready());
t.reset();
assert!(!t.is_ready());
}
#[test]
fn batch_equals_streaming() {
let candles: Vec<Candle> = (0..40_i64)
.map(|i| {
let base = 100.0 + (i as f64 * 0.25).sin() * 4.0;
candle(base + 1.0, base - 1.0, base + (i as f64 * 0.5).cos(), i)
})
.collect();
let mut a = TtmTrend::new(6).unwrap();
let mut b = TtmTrend::new(6).unwrap();
assert_eq!(
a.batch(&candles),
candles.iter().map(|c| b.update(*c)).collect::<Vec<_>>()
);
}
}
@@ -0,0 +1,212 @@
//! Wave PM — Cynthia Kase's peak-momentum statistic (Wickra reconstruction).
use std::collections::VecDeque;
use crate::error::{Error, Result};
use crate::indicators::ema::Ema;
use crate::traits::Indicator;
/// Wave PM (Peak Momentum): a `0..100` statistic that rises when the current
/// `length`-bar momentum is large relative to its own recent energy — Cynthia
/// Kase's gauge of how "peaked" the move is.
///
/// ```text
/// m = close_t - close_{t-length} (length-bar momentum)
/// energy = EMA(m^2, length) (mean squared momentum)
/// raw = 1 - exp( -m^2 / (2 * energy) ) (0 if energy == 0)
/// WavePM = 100 * EMA(raw, smoothing)
/// ```
///
/// The momentum `m` is normalised by its recent variance (`energy`): a move that
/// merely matches its typical energy sits at the baseline
/// `100·(1 e^{1/2}) ≈ 39.35`, while a momentum *spike* that exceeds recent
/// energy drives the reading toward `100`. A flat market (`m = 0`) reads `0`.
/// High readings mark a peaking, possibly exhausted move rather than a fresh one.
///
/// Kase's published `WavePM` is platform-specific; this is Wickra's faithful
/// reconstruction of its variance-normalised peak-momentum form. The exact
/// constants differ from any single vendor implementation, but the shape — flat
/// at zero, a fixed baseline on a steady trend, and saturation on an
/// acceleration — matches the indicator's intent.
///
/// Reference: Cynthia Kase, *Trading with the Odds*, 1996 (Wickra reconstruction).
///
/// # Example
///
/// ```
/// use wickra_core::{Indicator, WavePm};
///
/// let mut indicator = WavePm::new(10, 3).unwrap();
/// let mut last = None;
/// for i in 0..60 {
/// last = indicator.update(100.0 + f64::from(i));
/// }
/// assert!(last.is_some());
/// ```
#[derive(Debug, Clone)]
pub struct WavePm {
length: usize,
smoothing: usize,
closes: VecDeque<f64>,
energy_ema: Ema,
smooth_ema: Ema,
}
impl WavePm {
/// Construct a Wave PM with the momentum `length` and the output `smoothing`
/// period.
///
/// # Errors
///
/// Returns [`Error::PeriodZero`] if `length == 0` or `smoothing == 0`.
pub fn new(length: usize, smoothing: usize) -> Result<Self> {
if length == 0 {
return Err(Error::PeriodZero);
}
Ok(Self {
length,
smoothing,
closes: VecDeque::with_capacity(length + 1),
energy_ema: Ema::new(length)?,
smooth_ema: Ema::new(smoothing)?,
})
}
/// Configured `(length, smoothing)`.
pub const fn periods(&self) -> (usize, usize) {
(self.length, self.smoothing)
}
}
impl Indicator for WavePm {
type Input = f64;
type Output = f64;
fn update(&mut self, close: f64) -> Option<f64> {
self.closes.push_back(close);
if self.closes.len() > self.length + 1 {
self.closes.pop_front();
}
if self.closes.len() <= self.length {
return None;
}
let oldest = *self.closes.front().unwrap_or(&close);
let momentum = close - oldest;
let energy = self.energy_ema.update(momentum * momentum)?;
let raw = if energy <= 0.0 {
0.0
} else {
1.0 - (-(momentum * momentum) / (2.0 * energy)).exp()
};
self.smooth_ema.update(raw).map(|v| v * 100.0)
}
fn reset(&mut self) {
self.closes.clear();
self.energy_ema.reset();
self.smooth_ema.reset();
}
fn warmup_period(&self) -> usize {
2 * self.length + self.smoothing - 1
}
fn is_ready(&self) -> bool {
self.smooth_ema.is_ready()
}
fn name(&self) -> &'static str {
"WavePm"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
use approx::assert_relative_eq;
#[test]
fn rejects_zero_period() {
assert!(matches!(WavePm::new(0, 3), Err(Error::PeriodZero)));
assert!(matches!(WavePm::new(10, 0), Err(Error::PeriodZero)));
}
#[test]
fn accessors_and_metadata() {
let w = WavePm::new(10, 3).unwrap();
assert_eq!(w.periods(), (10, 3));
// 2*10 + 3 - 1 = 22.
assert_eq!(w.warmup_period(), 22);
assert_eq!(w.name(), "WavePm");
assert!(!w.is_ready());
}
#[test]
fn warmup_emits_at_expected_bar() {
let mut w = WavePm::new(3, 2).unwrap();
// warmup = 2*3 + 2 - 1 = 7 -> first value at input 7 (index 6).
let inputs: Vec<f64> = (0..12).map(f64::from).collect();
let out = w.batch(&inputs);
assert!(out[5].is_none());
assert!(out[6].is_some());
}
#[test]
fn flat_market_reads_zero() {
let mut w = WavePm::new(4, 2).unwrap();
let inputs = [50.0; 20];
let last = w.batch(&inputs).last().unwrap().unwrap();
assert_relative_eq!(last, 0.0, epsilon = 1e-12);
}
#[test]
fn steady_trend_reads_baseline() {
// Constant-slope ramp: momentum equals its own energy every bar, so the
// reading pins to the baseline 100*(1 - e^-0.5).
let mut w = WavePm::new(10, 3).unwrap();
let inputs: Vec<f64> = (0..60).map(|i| f64::from(i) * 5.0).collect();
let last = w.batch(&inputs).last().unwrap().unwrap();
let baseline = 100.0 * (1.0 - (-0.5_f64).exp());
assert_relative_eq!(last, baseline, epsilon = 1e-9);
}
#[test]
fn acceleration_reads_above_baseline() {
// A quadratic path: momentum keeps outrunning its lagged energy, so the
// reading sits above the steady-trend baseline.
let mut w = WavePm::new(10, 3).unwrap();
let inputs: Vec<f64> = (0..60).map(|i| f64::from(i * i) * 0.1).collect();
let last = w.batch(&inputs).last().unwrap().unwrap();
let baseline = 100.0 * (1.0 - (-0.5_f64).exp());
assert!(
last > baseline,
"accelerating wpm {last} should exceed {baseline}"
);
assert!(last <= 100.0, "wpm {last} must stay <= 100");
}
#[test]
fn reset_clears_state() {
let mut w = WavePm::new(10, 3).unwrap();
let inputs: Vec<f64> = (0..60).map(|i| f64::from(i) * 5.0).collect();
w.batch(&inputs);
assert!(w.is_ready());
w.reset();
assert!(!w.is_ready());
}
#[test]
fn batch_equals_streaming() {
let inputs: Vec<f64> = (0..80)
.map(|i| 100.0 + (f64::from(i) * 0.2).sin() * 5.0)
.collect();
let mut a = WavePm::new(10, 3).unwrap();
let mut b = WavePm::new(10, 3).unwrap();
assert_eq!(
a.batch(&inputs),
inputs.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
);
}
}
+55 -53
View File
@@ -84,59 +84,61 @@ pub use indicators::{
FibonacciPivots, FibonacciPivotsOutput, FisherRsi, FisherTransform, FlagPennant, Footprint,
FootprintOutput, ForceIndex, FractalChaosBands, FractalChaosBandsOutput, Frama, FundingBasis,
FundingRate, FundingRateMean, FundingRateZScore, GainLossRatio, GapSideBySideWhite,
GarmanKlassVolatility, Gartley, GeneralizedDema, GeometricMa, GoldenPocket, GoldenPocketOutput,
GrangerCausality, GravestoneDoji, Hammer, HangingMan, Harami, HeadAndShoulders, HeikinAshi,
HeikinAshiOutput, HiLoActivator, HighLowIndex, HighLowRange, HighWave, Hikkake,
HikkakeModified, HilbertDominantCycle, HistoricalVolatility, Hma, HoltWinters, HomingPigeon,
HtDcPhase, HtPhasor, HtPhasorOutput, HtTrendMode, HurstChannel, HurstChannelOutput,
HurstExponent, Ichimoku, IchimokuOutput, IdenticalThreeCrows, InNeck, Inertia,
InformationRatio, InitialBalance, InitialBalanceOutput, InstantaneousTrendline,
IntradayMomentumIndex, IntradayVolatilityProfile, IntradayVolatilityProfileOutput,
InverseFisherTransform, InvertedHammer, Jma, JumpIndicator, KagiBars, KalmanHedgeRatio,
KalmanHedgeRatioOutput, Kama, KellyCriterion, Keltner, KeltnerOutput, Kicking, KickingByLength,
Kst, KstOutput, Kurtosis, Kvo, KylesLambda, LadderBottom, LaguerreRsi, LeadLagCrossCorrelation,
LeadLagCrossCorrelationOutput, LinRegAngle, LinRegChannel, LinRegChannelOutput,
LinRegIntercept, LinRegSlope, LinearRegression, LiquidationFeatures, LiquidationFeaturesOutput,
LogReturn, LongLeggedDoji, LongLine, LongShortRatio, MaEnvelope, MaEnvelopeOutput, MacdExt,
MacdFix, MacdIndicator, MacdOutput, Mama, MamaOutput, MarketFacilitationIndex, Marubozu,
MassIndex, MatHold, MatchingLow, MaxDrawdown, McClellanOscillator, McClellanSummationIndex,
McGinleyDynamic, MedianAbsoluteDeviation, MedianMa, MedianPrice, Mfi, Microprice, MidPoint,
MidPrice, MinusDi, MinusDm, Mom, MorningDojiStar, MorningEveningStar, Natr, NewHighsNewLows,
Nvi, OIPriceDivergence, OIWeighted, Obv, OmegaRatio, OnNeck, OpenInterestDelta,
OpeningMarubozu, OpeningRange, OpeningRangeOutput, OrderBookImbalanceFull,
OrderBookImbalanceTop1, OrderBookImbalanceTopN, OrderFlowImbalance, OuHalfLife, OvernightGap,
OvernightIntradayReturn, OvernightIntradayReturnOutput, PainIndex, PairSpreadZScore,
PairwiseBeta, ParkinsonVolatility, PearsonCorrelation, PercentAboveMa, PercentB,
PercentageTrailingStop, Pgo, PiercingDarkCloud, PlusDi, PlusDm, Pmo, PointAndFigureBars, Ppo,
ProfitFactor, Psar, Pvi, Qqe, QqeOutput, QuotedSpread, RSquared, RealizedSpread,
RealizedVolatility, RecoveryFactor, RectangleRange, RegimeLabel, RelativeStrengthAB,
RelativeStrengthOutput, RenkoBars, RenkoTrailingStop, RickshawMan, RisingThreeMethods, Rmi,
Roc, Rocp, Rocr, Rocr100, RogersSatchellVolatility, RollMeasure, RollingCorrelation,
RollingCovariance, RollingIqr, RollingPercentileRank, RollingQuantile, RollingVwap,
RoofingFilter, Rsi, Rsx, Rvi, RviVolatility, Rwi, RwiOutput, SarExt, SeasonalZScore,
SeparatingLines, SessionHighLow, SessionHighLowOutput, SessionRange, SessionRangeOutput,
SessionVwap, Shark, SharpeRatio, ShootingStar, ShortLine, SignedVolume, SineWave,
SineWeightedMa, Skewness, Sma, Smi, Smma, SortinoRatio, SpearmanCorrelation, SpinningTop,
SpreadAr1Coefficient, SpreadBollingerBands, SpreadBollingerBandsOutput, SpreadHurst,
StalledPattern, StandardError, StandardErrorBands, StandardErrorBandsOutput, StarcBands,
StarcBandsOutput, Stc, StdDev, StepTrailingStop, StickSandwich, StochRsi, Stochastic,
StochasticCci, StochasticOutput, SuperSmoother, SuperTrend, SuperTrendOutput,
TakerBuySellRatio, Takuri, TasukiGap, TdCombo, TdCountdown, TdDeMarker, TdDifferential,
TdLines, TdLinesOutput, TdOpen, TdPressure, TdRangeProjection, TdRangeProjectionOutput, TdRei,
TdRiskLevel, TdRiskLevelOutput, TdSequential, TdSequentialOutput, TdSetup, Tema,
TermStructureBasis, ThreeDrives, ThreeInside, ThreeLineStrike, ThreeOutside,
ThreeSoldiersOrCrows, ThreeStarsInSouth, Thrusting, TickIndex, Tii, TimeOfDayReturnProfile,
TimeOfDayReturnProfileOutput, TpoProfile, TpoProfileOutput, TradeImbalance, TrendLabel,
TreynorRatio, Triangle, Trima, Trin, TripleTopBottom, Trix, TrueRange, Tsf, Tsi, Tsv,
TtmSqueeze, TtmSqueezeOutput, TurnOfMonth, Tweezer, TwoCrows, TypicalPrice, UlcerIndex,
UltimateOscillator, UniqueThreeRiver, UpDownVolumeRatio, UpsideGapThreeMethods,
UpsideGapTwoCrows, ValueArea, ValueAreaOutput, ValueAtRisk, Variance, VarianceRatio,
VerticalHorizontalFilter, Vidya, VoltyStop, VolumeByTimeProfile, VolumeByTimeProfileOutput,
VolumeOscillator, VolumePriceTrend, VolumeProfile, VolumeProfileOutput, Vortex, VortexOutput,
Vpin, Vwap, VwapStdDevBands, VwapStdDevBandsOutput, Vwma, Vzo, WaveTrend, WaveTrendOutput,
Wedge, WeightedClose, WickRatio, WilliamsFractals, WilliamsFractalsOutput, WilliamsR, WinRate,
Wma, WoodiePivots, WoodiePivotsOutput, YangZhangVolatility, YoyoExit, ZScore, ZeroLagMacd,
ZeroLagMacdOutput, ZigZag, ZigZagOutput, Zlema, FAMILIES, T3,
GarmanKlassVolatility, Gartley, GatorOscillator, GatorOscillatorOutput, GeneralizedDema,
GeometricMa, GoldenPocket, GoldenPocketOutput, GrangerCausality, GravestoneDoji, Hammer,
HangingMan, Harami, HeadAndShoulders, HeikinAshi, HeikinAshiOutput, HiLoActivator,
HighLowIndex, HighLowRange, HighWave, Hikkake, HikkakeModified, HilbertDominantCycle,
HistoricalVolatility, Hma, HoltWinters, HomingPigeon, HtDcPhase, HtPhasor, HtPhasorOutput,
HtTrendMode, HurstChannel, HurstChannelOutput, HurstExponent, Ichimoku, IchimokuOutput,
IdenticalThreeCrows, InNeck, Inertia, InformationRatio, InitialBalance, InitialBalanceOutput,
InstantaneousTrendline, IntradayMomentumIndex, IntradayVolatilityProfile,
IntradayVolatilityProfileOutput, InverseFisherTransform, InvertedHammer, Jma, JumpIndicator,
KagiBars, KalmanHedgeRatio, KalmanHedgeRatioOutput, Kama, KasePermissionStochastic,
KasePermissionStochasticOutput, KellyCriterion, Keltner, KeltnerOutput, Kicking,
KickingByLength, Kst, KstOutput, Kurtosis, Kvo, KylesLambda, LadderBottom, LaguerreRsi,
LeadLagCrossCorrelation, LeadLagCrossCorrelationOutput, LinRegAngle, LinRegChannel,
LinRegChannelOutput, LinRegIntercept, LinRegSlope, LinearRegression, LiquidationFeatures,
LiquidationFeaturesOutput, LogReturn, LongLeggedDoji, LongLine, LongShortRatio, MaEnvelope,
MaEnvelopeOutput, MacdExt, MacdFix, MacdHistogram, MacdIndicator, MacdOutput, Mama, MamaOutput,
MarketFacilitationIndex, Marubozu, MassIndex, MatHold, MatchingLow, MaxDrawdown,
McClellanOscillator, McClellanSummationIndex, McGinleyDynamic, MedianAbsoluteDeviation,
MedianMa, MedianPrice, Mfi, Microprice, MidPoint, MidPrice, MinusDi, MinusDm, Mom,
MorningDojiStar, MorningEveningStar, Natr, NewHighsNewLows, Nvi, OIPriceDivergence, OIWeighted,
Obv, OmegaRatio, OnNeck, OpenInterestDelta, OpeningMarubozu, OpeningRange, OpeningRangeOutput,
OrderBookImbalanceFull, OrderBookImbalanceTop1, OrderBookImbalanceTopN, OrderFlowImbalance,
OuHalfLife, OvernightGap, OvernightIntradayReturn, OvernightIntradayReturnOutput, PainIndex,
PairSpreadZScore, PairwiseBeta, ParkinsonVolatility, PearsonCorrelation, PercentAboveMa,
PercentB, PercentageTrailingStop, Pgo, PiercingDarkCloud, PlusDi, PlusDm, Pmo,
PointAndFigureBars, PolarizedFractalEfficiency, Ppo, PpoHistogram, ProfitFactor, Psar, Pvi,
Qqe, QqeOutput, Qstick, QuotedSpread, RSquared, RealizedSpread, RealizedVolatility,
RecoveryFactor, RectangleRange, RegimeLabel, RelativeStrengthAB, RelativeStrengthOutput,
RenkoBars, RenkoTrailingStop, RickshawMan, RisingThreeMethods, Rmi, Roc, Rocp, Rocr, Rocr100,
RogersSatchellVolatility, RollMeasure, RollingCorrelation, RollingCovariance, RollingIqr,
RollingPercentileRank, RollingQuantile, RollingVwap, RoofingFilter, Rsi, Rsx, Rvi,
RviVolatility, Rwi, RwiOutput, SarExt, SeasonalZScore, SeparatingLines, SessionHighLow,
SessionHighLowOutput, SessionRange, SessionRangeOutput, SessionVwap, Shark, SharpeRatio,
ShootingStar, ShortLine, SignedVolume, SineWave, SineWeightedMa, Skewness, Sma, Smi, Smma,
SortinoRatio, SpearmanCorrelation, SpinningTop, SpreadAr1Coefficient, SpreadBollingerBands,
SpreadBollingerBandsOutput, SpreadHurst, StalledPattern, StandardError, StandardErrorBands,
StandardErrorBandsOutput, StarcBands, StarcBandsOutput, Stc, StdDev, StepTrailingStop,
StickSandwich, StochRsi, Stochastic, StochasticCci, StochasticOutput, SuperSmoother,
SuperTrend, SuperTrendOutput, TakerBuySellRatio, Takuri, TasukiGap, TdCombo, TdCountdown,
TdDeMarker, TdDifferential, TdLines, TdLinesOutput, TdOpen, TdPressure, TdRangeProjection,
TdRangeProjectionOutput, TdRei, TdRiskLevel, TdRiskLevelOutput, TdSequential,
TdSequentialOutput, TdSetup, Tema, TermStructureBasis, ThreeDrives, ThreeInside,
ThreeLineStrike, ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth, Thrusting, TickIndex,
Tii, TimeOfDayReturnProfile, TimeOfDayReturnProfileOutput, TpoProfile, TpoProfileOutput,
TradeImbalance, TrendLabel, TrendStrengthIndex, TreynorRatio, Triangle, Trima, Trin,
TripleTopBottom, Trix, TrueRange, Tsf, TsfOscillator, Tsi, Tsv, TtmSqueeze, TtmSqueezeOutput,
TtmTrend, TurnOfMonth, Tweezer, TwoCrows, TypicalPrice, UlcerIndex, UltimateOscillator,
UniqueThreeRiver, UpDownVolumeRatio, UpsideGapThreeMethods, UpsideGapTwoCrows, ValueArea,
ValueAreaOutput, ValueAtRisk, Variance, VarianceRatio, VerticalHorizontalFilter, Vidya,
VoltyStop, VolumeByTimeProfile, VolumeByTimeProfileOutput, VolumeOscillator, VolumePriceTrend,
VolumeProfile, VolumeProfileOutput, Vortex, VortexOutput, Vpin, Vwap, VwapStdDevBands,
VwapStdDevBandsOutput, Vwma, Vzo, WavePm, WaveTrend, WaveTrendOutput, Wedge, WeightedClose,
WickRatio, WilliamsFractals, WilliamsFractalsOutput, WilliamsR, WinRate, Wma, WoodiePivots,
WoodiePivotsOutput, YangZhangVolatility, YoyoExit, ZScore, ZeroLagMacd, ZeroLagMacdOutput,
ZigZag, ZigZagOutput, Zlema, FAMILIES, T3,
};
// `FootprintLevel` is a row element of `FootprintOutput`, re-exported on its own
// line so the indicator-count tooling (which scans the braced block above and
+1 -1
View File
@@ -8,7 +8,7 @@ That includes:
[Python](https://docs.wickra.org/Quickstart-Python),
[Node](https://docs.wickra.org/Quickstart-Node), and
[WASM](https://docs.wickra.org/Quickstart-WASM).
- A per-indicator deep dive for every one of the **413 indicators** across
- A per-indicator deep dive for every one of the **423 indicators** across
the sixteen families (Moving Averages, Momentum Oscillators, Trend &
Directional, Price Oscillators, Volatility & Bands, Bands & Channels,
Trailing Stops, Volume, Price Statistics, Ehlers / Cycle DSP, Pivots &
+7 -7
View File
@@ -17,7 +17,7 @@
},
"../../bindings/node": {
"name": "wickra",
"version": "0.5.6",
"version": "0.5.8",
"license": "MIT OR Apache-2.0",
"devDependencies": {
"@napi-rs/cli": "^2.18.0"
@@ -26,12 +26,12 @@
"node": ">= 18"
},
"optionalDependencies": {
"wickra-darwin-arm64": "0.5.6",
"wickra-darwin-x64": "0.5.6",
"wickra-linux-arm64-gnu": "0.5.6",
"wickra-linux-x64-gnu": "0.5.6",
"wickra-win32-arm64-msvc": "0.5.6",
"wickra-win32-x64-msvc": "0.5.6"
"wickra-darwin-arm64": "0.5.8",
"wickra-darwin-x64": "0.5.8",
"wickra-linux-arm64-gnu": "0.5.8",
"wickra-linux-x64-gnu": "0.5.8",
"wickra-win32-arm64-msvc": "0.5.8",
"wickra-win32-x64-msvc": "0.5.8"
}
},
"node_modules/wickra": {
+7 -1
View File
@@ -14,7 +14,7 @@
//! `Ema(20)`. This target now covers every scalar indicator in the catalogue.
use libfuzzer_sys::fuzz_target;
use wickra_core::{AdaptiveCycle, AdaptiveLaguerreFilter, Alma, AnchoredRsi, Apo, Autocorrelation, AverageDrawdown, BatchExt, Beta, BollingerBands, CalmarRatio, CenterOfGravity, Cfo, Cmo, CoefficientOfVariation, ConditionalValueAtRisk, ConnorsRsi, Coppock, CyberneticCycle, Decycler, DecyclerOscillator, Dema, DerivativeOscillator, DetrendedStdDev, DisparityIndex, DoubleBollinger, Dpo, DrawdownDuration, DynamicMomentumIndex, EhlersStochastic, Ehma, ElderImpulse, Ema, EmpiricalModeDecomposition, Expectancy, Fama, FisherRsi, FisherTransform, Frama, GainLossRatio, GeneralizedDema, GeometricMa, HilbertDominantCycle, HistoricalVolatility, Hma, HoltWinters, HtDcPhase, HtPhasor, HtTrendMode, HurstExponent, Indicator, InstantaneousTrendline, InverseFisherTransform, Jma, JumpIndicator, Kama, KellyCriterion, Kst, Kurtosis, LaguerreRsi, LinRegAngle, LinRegChannel, LinRegIntercept, LinRegSlope, LinearRegression, LogReturn, MaEnvelope, MaType, MacdExt, MacdFix, MacdIndicator, Mama, MaxDrawdown, McGinleyDynamic, MedianAbsoluteDeviation, MedianMa, MidPoint, Mom, OmegaRatio, PainIndex, PearsonCorrelation, PercentageTrailingStop, Pmo, Ppo, ProfitFactor, Qqe, RSquared, RealizedVolatility, RecoveryFactor, RegimeLabel, RenkoTrailingStop, Rmi, Roc, Rocp, Rocr, Rocr100, RollingIqr, RollingPercentileRank, RollingQuantile, RoofingFilter, Rsi, Rsx, RviVolatility, SharpeRatio, SineWave, SineWeightedMa, Skewness, Sma, Smma, SortinoRatio, SpearmanCorrelation, StandardError, StandardErrorBands, Stc, StdDev, StepTrailingStop, StochRsi, SuperSmoother, Tema, Tii, TrendLabel, Trima, Trix, Tsf, Tsi, UlcerIndex, ValueAtRisk, Variance, VerticalHorizontalFilter, Vidya, WinRate, Wma, ZScore, ZeroLagMacd, Zlema, T3};
use wickra_core::{AdaptiveCycle, AdaptiveLaguerreFilter, Alma, AnchoredRsi, Apo, Autocorrelation, AverageDrawdown, BatchExt, Beta, BollingerBands, CalmarRatio, CenterOfGravity, Cfo, Cmo, CoefficientOfVariation, ConditionalValueAtRisk, ConnorsRsi, Coppock, CyberneticCycle, Decycler, DecyclerOscillator, Dema, DerivativeOscillator, DetrendedStdDev, DisparityIndex, DoubleBollinger, Dpo, DrawdownDuration, DynamicMomentumIndex, EhlersStochastic, Ehma, ElderImpulse, Ema, EmpiricalModeDecomposition, Expectancy, Fama, FisherRsi, FisherTransform, Frama, GainLossRatio, GeneralizedDema, GeometricMa, HilbertDominantCycle, HistoricalVolatility, Hma, HoltWinters, HtDcPhase, HtPhasor, HtTrendMode, HurstExponent, Indicator, InstantaneousTrendline, InverseFisherTransform, Jma, JumpIndicator, Kama, KellyCriterion, Kst, Kurtosis, LaguerreRsi, LinRegAngle, LinRegChannel, LinRegIntercept, LinRegSlope, LinearRegression, LogReturn, MaEnvelope, MaType, MacdExt, MacdFix, MacdHistogram, MacdIndicator, Mama, MaxDrawdown, McGinleyDynamic, MedianAbsoluteDeviation, MedianMa, MidPoint, Mom, OmegaRatio, PainIndex, PearsonCorrelation, PercentageTrailingStop, Pmo, PolarizedFractalEfficiency, Ppo, PpoHistogram, ProfitFactor, Qqe, RSquared, RealizedVolatility, RecoveryFactor, RegimeLabel, RenkoTrailingStop, Rmi, Roc, Rocp, Rocr, Rocr100, RollingIqr, RollingPercentileRank, RollingQuantile, RoofingFilter, Rsi, Rsx, RviVolatility, SharpeRatio, SineWave, SineWeightedMa, Skewness, Sma, Smma, SortinoRatio, SpearmanCorrelation, StandardError, StandardErrorBands, Stc, StdDev, StepTrailingStop, StochRsi, SuperSmoother, Tema, Tii, TrendLabel, TrendStrengthIndex, Trima, Trix, Tsf, TsfOscillator, Tsi, UlcerIndex, ValueAtRisk, Variance, VerticalHorizontalFilter, Vidya, WavePm, WinRate, Wma, ZScore, ZeroLagMacd, Zlema, T3};
/// Drive a single streaming + batch run through one scalar indicator. Marked
/// `#[inline(never)]` so a panic backtrace pin-points the specific indicator.
@@ -73,6 +73,9 @@ fuzz_target!(|data: Vec<f64>| {
drive(|| DynamicMomentumIndex::new(14).unwrap(), &data);
drive(|| Rmi::new(14, 5).unwrap(), &data);
drive(|| DerivativeOscillator::new(14, 5, 3, 9).unwrap(), &data);
drive(|| TrendStrengthIndex::new(20).unwrap(), &data);
drive(|| PolarizedFractalEfficiency::new(10, 5).unwrap(), &data);
drive(|| WavePm::new(32, 3).unwrap(), &data);
drive(|| Tsi::new(25, 13).unwrap(), &data);
drive(|| Pmo::new(35, 20).unwrap(), &data);
drive(|| Tii::new(60, 30).unwrap(), &data);
@@ -81,6 +84,9 @@ fuzz_target!(|data: Vec<f64>| {
drive(|| Ppo::new(12, 26).unwrap(), &data);
drive(|| Apo::new(12, 26).unwrap(), &data);
drive(|| Cfo::new(14).unwrap(), &data);
drive(|| TsfOscillator::new(14).unwrap(), &data);
drive(|| MacdHistogram::new(12, 26, 9).unwrap(), &data);
drive(|| PpoHistogram::new(12, 26, 9).unwrap(), &data);
drive(|| ElderImpulse::classic(), &data);
drive(|| Stc::classic(), &data);
drive(|| Coppock::new(14, 11, 10).unwrap(), &data);
+5 -1
View File
@@ -22,7 +22,7 @@
//! WeightedClose.
use libfuzzer_sys::fuzz_target;
use wickra_core::{AbandonedBaby, Abcd, AccelerationBands, AcceleratorOscillator, AdOscillator, Adl, AdvanceBlock, Adx, Adxr, Alligator, AnchoredVwap, Aroon, AroonOscillator, Atr, AtrBands, AtrTrailingStop, AutoFib, AverageDailyRange, AwesomeOscillator, AwesomeOscillatorHistogram, BalanceOfPower, Bat, BatchExt, BeltHold, BodySizePct, Breakaway, Butterfly, Camarilla, Candle, Cci, ChaikinMoneyFlow, ChaikinOscillator, ChaikinVolatility, ChandeKrollStop, ChandelierExit, ChoppinessIndex, ClassicPivots, CloseVsOpen, ClosingMarubozu, ConcealingBabySwallow, Counterattack, Crab, CupAndHandle, Cypher, DayOfWeekProfile, DemandIndex, DemarkPivots, Doji, DojiStar, Donchian, DonchianStop, DoubleTopBottom, DownsideGapThreeMethods, DragonflyDoji, Dx, EaseOfMovement, ElderRay, Engulfing, EveningDojiStar, Evwma, FallingThreeMethods, FibArcs, FibChannel, FibConfluence, FibExtension, FibFan, FibProjection, FibRetracement, FibTimeZones, FibonacciPivots, FlagPennant, ForceIndex, FractalChaosBands, GapSideBySideWhite, GarmanKlassVolatility, Gartley, GoldenPocket, GravestoneDoji, Hammer, HangingMan, Harami, HeadAndShoulders, HeikinAshi, HiLoActivator, HighLowRange, HighWave, Hikkake, HikkakeModified, HomingPigeon, HurstChannel, Ichimoku, IdenticalThreeCrows, InNeck, Indicator, Inertia, InitialBalance, IntradayMomentumIndex, IntradayVolatilityProfile, InvertedHammer, Keltner, Kicking, KickingByLength, Kvo, LadderBottom, LongLeggedDoji, LongLine, MarketFacilitationIndex, Marubozu, MassIndex, MatHold, MatchingLow, AvgPrice, MedianPrice, Mfi, MidPrice, MinusDi, MinusDm, MorningDojiStar, MorningEveningStar, Natr, Nvi, Obv, OnNeck, OpeningMarubozu, OpeningRange, OvernightGap, OvernightIntradayReturn, ParkinsonVolatility, Pgo, PiercingDarkCloud, PlusDi, PlusDm, Psar, Pvi, RectangleRange, RickshawMan, RisingThreeMethods, RogersSatchellVolatility, RollingVwap, Rvi, Rwi, SarExt, SeasonalZScore, SeparatingLines, SessionHighLow, SessionRange, SessionVwap, Shark, ShootingStar, ShortLine, Smi, SpinningTop, StalledPattern, StarcBands, StickSandwich, Stochastic, StochasticCci, SuperTrend, Takuri, TasukiGap, TdCombo, TdCountdown, TdDeMarker, TdDifferential, TdLines, TdOpen, TdPressure, TdRangeProjection, TdRei, TdRiskLevel, TdSequential, TdSetup, ThreeDrives, ThreeInside, ThreeLineStrike, ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth, Thrusting, TimeOfDayReturnProfile, TpoProfile, Triangle, TripleTopBottom, TrueRange, Tsv, TtmSqueeze, TurnOfMonth, Tweezer, TwoCrows, TypicalPrice, UltimateOscillator, UniqueThreeRiver, UpsideGapThreeMethods, UpsideGapTwoCrows, ValueArea, VoltyStop, VolumeByTimeProfile, VolumeOscillator, VolumePriceTrend, VolumeProfile, Vortex, Vwap, VwapStdDevBands, Vwma, Vzo, WaveTrend, Wedge, WeightedClose, WickRatio, WilliamsFractals, WilliamsR, WoodiePivots, YangZhangVolatility, YoyoExit, ZigZag};
use wickra_core::{AbandonedBaby, Abcd, AccelerationBands, AcceleratorOscillator, AdOscillator, Adl, AdvanceBlock, Adx, Adxr, Alligator, AnchoredVwap, Aroon, AroonOscillator, Atr, AtrBands, AtrTrailingStop, AutoFib, AverageDailyRange, AwesomeOscillator, AwesomeOscillatorHistogram, BalanceOfPower, Bat, BatchExt, BeltHold, BodySizePct, Breakaway, Butterfly, Camarilla, Candle, Cci, ChaikinMoneyFlow, ChaikinOscillator, ChaikinVolatility, ChandeKrollStop, ChandelierExit, ChoppinessIndex, ClassicPivots, CloseVsOpen, ClosingMarubozu, ConcealingBabySwallow, Counterattack, Crab, CupAndHandle, Cypher, DayOfWeekProfile, DemandIndex, DemarkPivots, Doji, DojiStar, Donchian, DonchianStop, DoubleTopBottom, DownsideGapThreeMethods, DragonflyDoji, Dx, EaseOfMovement, ElderRay, Engulfing, EveningDojiStar, Evwma, FallingThreeMethods, FibArcs, FibChannel, FibConfluence, FibExtension, FibFan, FibProjection, FibRetracement, FibTimeZones, FibonacciPivots, FlagPennant, ForceIndex, FractalChaosBands, GapSideBySideWhite, GarmanKlassVolatility, Gartley, GatorOscillator, GoldenPocket, GravestoneDoji, Hammer, HangingMan, Harami, HeadAndShoulders, HeikinAshi, HiLoActivator, HighLowRange, HighWave, Hikkake, HikkakeModified, HomingPigeon, HurstChannel, Ichimoku, IdenticalThreeCrows, InNeck, Indicator, Inertia, InitialBalance, IntradayMomentumIndex, IntradayVolatilityProfile, InvertedHammer, KasePermissionStochastic, Keltner, Kicking, KickingByLength, Kvo, LadderBottom, LongLeggedDoji, LongLine, MarketFacilitationIndex, Marubozu, MassIndex, MatHold, MatchingLow, AvgPrice, MedianPrice, Mfi, MidPrice, MinusDi, MinusDm, MorningDojiStar, MorningEveningStar, Natr, Nvi, Obv, OnNeck, OpeningMarubozu, OpeningRange, OvernightGap, OvernightIntradayReturn, ParkinsonVolatility, Pgo, PiercingDarkCloud, PlusDi, PlusDm, Psar, Pvi, Qstick, RectangleRange, RickshawMan, RisingThreeMethods, RogersSatchellVolatility, RollingVwap, Rvi, Rwi, SarExt, SeasonalZScore, SeparatingLines, SessionHighLow, SessionRange, SessionVwap, Shark, ShootingStar, ShortLine, Smi, SpinningTop, StalledPattern, StarcBands, StickSandwich, Stochastic, StochasticCci, SuperTrend, Takuri, TasukiGap, TdCombo, TdCountdown, TdDeMarker, TdDifferential, TdLines, TdOpen, TdPressure, TdRangeProjection, TdRei, TdRiskLevel, TdSequential, TdSetup, ThreeDrives, ThreeInside, ThreeLineStrike, ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth, Thrusting, TimeOfDayReturnProfile, TpoProfile, Triangle, TripleTopBottom, TrueRange, Tsv, TtmSqueeze, TtmTrend, TurnOfMonth, Tweezer, TwoCrows, TypicalPrice, UltimateOscillator, UniqueThreeRiver, UpsideGapThreeMethods, UpsideGapTwoCrows, ValueArea, VoltyStop, VolumeByTimeProfile, VolumeOscillator, VolumePriceTrend, VolumeProfile, Vortex, Vwap, VwapStdDevBands, Vwma, Vzo, WaveTrend, Wedge, WeightedClose, WickRatio, WilliamsFractals, WilliamsR, WoodiePivots, YangZhangVolatility, YoyoExit, ZigZag};
/// Convert a flat `f64` stream into a `Vec<Candle>` by chunking it into
/// `[open, high, low, close, volume]` groups. Tuples that fail OHLCV
@@ -87,6 +87,10 @@ fuzz_target!(|data: Vec<f64>| {
drive(|| YoyoExit::new(14, 2.0).unwrap(), &candles);
// --- Trend & Directional ---
drive(|| KasePermissionStochastic::new(9, 3).unwrap(), &candles);
drive(|| GatorOscillator::new(13, 8, 5).unwrap(), &candles);
drive(|| Qstick::new(10).unwrap(), &candles);
drive(|| TtmTrend::new(6).unwrap(), &candles);
drive(|| Adx::new(14).unwrap(), &candles);
drive(|| Adxr::new(14).unwrap(), &candles);
drive(|| PlusDm::new(14).unwrap(), &candles);