Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 83e34c6f71 | |||
| 67feec598a |
+9
-1
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.6.1] - 2026-06-07
|
||||
- **Projection Oscillator** — Widner projection oscillator: close position inside the projection bands, scaled 0..100 (`ProjectionOscillator`).
|
||||
- **Projection Bands** — Widner projection bands: forward-projected high/low regression envelope (`ProjectionBands`).
|
||||
- **Median Channel** — robust median +/- multiplier*MAD envelope (`MedianChannel`).
|
||||
- **Bomar Bands** — adaptive percentage bands containing a target coverage fraction of recent closes (`BomarBands`).
|
||||
- **Quartile Bands** — rolling 25th/50th/75th-percentile (Q1/median/Q3) envelope (`QuartileBands`).
|
||||
|
||||
## [0.6.0] - 2026-06-06
|
||||
- **Volatility Cone** — volatility cone: current realized volatility within its historical min/median/max envelope (`VolatilityCone`).
|
||||
- **VolatilityRatio** — Schwager's volatility ratio: true range over the EMA of prior true ranges (`VolatilityRatio`).
|
||||
@@ -1301,7 +1308,8 @@ 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.6.0...HEAD
|
||||
[Unreleased]: https://github.com/wickra-lib/wickra/compare/v0.6.1...HEAD
|
||||
[0.6.1]: https://github.com/wickra-lib/wickra/compare/v0.6.0...v0.6.1
|
||||
[0.6.0]: https://github.com/wickra-lib/wickra/compare/v0.5.9...v0.6.0
|
||||
[0.5.9]: https://github.com/wickra-lib/wickra/compare/v0.5.8...v0.5.9
|
||||
[0.5.8]: https://github.com/wickra-lib/wickra/compare/v0.5.7...v0.5.8
|
||||
|
||||
Generated
+8
-8
@@ -1944,7 +1944,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra"
|
||||
version = "0.6.0"
|
||||
version = "0.6.1"
|
||||
dependencies = [
|
||||
"approx",
|
||||
"criterion",
|
||||
@@ -1955,7 +1955,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-bench"
|
||||
version = "0.6.0"
|
||||
version = "0.6.1"
|
||||
dependencies = [
|
||||
"criterion",
|
||||
"kand",
|
||||
@@ -1967,7 +1967,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-core"
|
||||
version = "0.6.0"
|
||||
version = "0.6.1"
|
||||
dependencies = [
|
||||
"approx",
|
||||
"proptest",
|
||||
@@ -1977,7 +1977,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-data"
|
||||
version = "0.6.0"
|
||||
version = "0.6.1"
|
||||
dependencies = [
|
||||
"approx",
|
||||
"csv",
|
||||
@@ -1994,7 +1994,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-examples"
|
||||
version = "0.6.0"
|
||||
version = "0.6.1"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
"tokio",
|
||||
@@ -2004,7 +2004,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-node"
|
||||
version = "0.6.0"
|
||||
version = "0.6.1"
|
||||
dependencies = [
|
||||
"napi",
|
||||
"napi-build",
|
||||
@@ -2014,7 +2014,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-python"
|
||||
version = "0.6.0"
|
||||
version = "0.6.1"
|
||||
dependencies = [
|
||||
"numpy",
|
||||
"pyo3",
|
||||
@@ -2023,7 +2023,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-wasm"
|
||||
version = "0.6.0"
|
||||
version = "0.6.1"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"js-sys",
|
||||
|
||||
+2
-2
@@ -13,7 +13,7 @@ members = [
|
||||
exclude = ["fuzz"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.6.0"
|
||||
version = "0.6.1"
|
||||
authors = ["kingchenc <support@wickra.org>"]
|
||||
edition = "2021"
|
||||
rust-version = "1.86"
|
||||
@@ -25,7 +25,7 @@ keywords = ["finance", "trading", "indicators", "technical-analysis", "ta"]
|
||||
categories = ["finance", "mathematics", "science"]
|
||||
|
||||
[workspace.dependencies]
|
||||
wickra-core = { path = "crates/wickra-core", version = "0.6.0" }
|
||||
wickra-core = { path = "crates/wickra-core", version = "0.6.1" }
|
||||
|
||||
thiserror = "2"
|
||||
rayon = "1.10"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<p align="center">
|
||||
<a href="https://wickra.org"><img src="https://raw.githubusercontent.com/wickra-lib/.github/main/profile/wickra-banner.webp?v=429" 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=434" alt="Wickra — streaming-first technical indicators" width="100%"></a>
|
||||
</p>
|
||||
|
||||
[](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml)
|
||||
@@ -48,7 +48,7 @@ Full documentation lives at **[docs.wickra.org](https://docs.wickra.org)**:
|
||||
[Node](https://docs.wickra.org/Quickstart-Node),
|
||||
[WASM](https://docs.wickra.org/Quickstart-WASM).
|
||||
- **Indicators** — a per-indicator deep dive (formula, parameters, warmup) for
|
||||
every one of the 429 indicators; start at the
|
||||
every one of the 434 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),
|
||||
@@ -79,7 +79,7 @@ Plenty of TA libraries are fast. Each one forces a trade-off Wickra does not:
|
||||
| finta | clean | no | Python | ~80 | stale |
|
||||
| talipp | clean | yes | Python | ~40 | yes |
|
||||
|
||||
Wickra's edge is **breadth with reach**: 429 indicators that all update in O(1)
|
||||
Wickra's edge is **breadth with reach**: 434 indicators that all update in O(1)
|
||||
per tick and ship natively to Python, Node.js, WebAssembly and Rust from a
|
||||
single engine.
|
||||
|
||||
@@ -188,7 +188,7 @@ python -m benchmarks.compare_libraries
|
||||
|
||||
## Indicators
|
||||
|
||||
429 streaming-first indicators across twenty-four families. Every one passes the
|
||||
434 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).
|
||||
@@ -200,7 +200,7 @@ warmup) at [docs.wickra.org](https://docs.wickra.org/Indicators-Overview).
|
||||
| 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, Volatility Cone |
|
||||
| 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 |
|
||||
| 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, Quartile Bands, Bomar Bands, Median Channel, Projection Bands, Projection Oscillator |
|
||||
| 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 |
|
||||
| Volume | OBV, VWAP (cumulative + rolling), ADL, Volume-Price Trend, Chaikin Money Flow, Chaikin Oscillator, Force Index, Ease of Movement, Klinger Volume Oscillator, Volume Oscillator, NVI, PVI, Williams A/D, Anchored VWAP, Demand Index, TSV, VZO, Market Facilitation Index |
|
||||
| Price Statistics | Typical Price, Median Price, Weighted Close, Linear Regression, Linear Regression Slope, Z-Score, Linear Regression Angle, Variance, Coefficient of Variation, Skewness, Kurtosis, Standard Error, Detrended StdDev, R², Median Absolute Deviation, Autocorrelation, Hurst Exponent, Pearson Correlation, Beta, Pairwise Beta, Pair Spread Z-Score, Lead-Lag Cross-Correlation, Cointegration, Relative Strength A-vs-B, Spearman Correlation, Mid Price, Mid Point, Average Price, Linear Regression Intercept, Time Series Forecast, Rolling Correlation, Rolling Covariance, OU Half-Life, Spread Hurst, Distance SSD, Beta-Neutral Spread, Variance Ratio, Granger Causality, Kalman Hedge Ratio, Spread Bollinger Bands, Spread AR(1) Coefficient |
|
||||
@@ -297,7 +297,7 @@ A Python live-trading example using the public `websockets` package lives at
|
||||
```
|
||||
wickra/
|
||||
├── crates/
|
||||
│ ├── wickra-core/ core engine + all 429 indicators
|
||||
│ ├── wickra-core/ core engine + all 434 indicators
|
||||
│ ├── wickra/ top-level facade crate (publishes on crates.io) + benches/
|
||||
│ ├── wickra-data/ CSV reader, tick aggregator, live exchange feeds
|
||||
│ └── wickra-bench/ internal cross-library benchmark harness (not published)
|
||||
|
||||
@@ -355,6 +355,7 @@ const candleScalar = {
|
||||
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) },
|
||||
VolatilityRatio: { make: () => new wickra.VolatilityRatio(14), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
|
||||
ProjectionOscillator: { make: () => new wickra.ProjectionOscillator(14), 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(candleScalar)) {
|
||||
@@ -442,6 +443,10 @@ const multi = {
|
||||
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) },
|
||||
VolatilityCone: { make: () => new wickra.VolatilityCone(20, 60), fields: ['current', 'min', 'median', 'max', 'percentile'], step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
|
||||
QuartileBands: { make: () => new wickra.QuartileBands(4), fields: ['upper', 'middle', 'lower'], step: (ind, i) => ind.update(close[i]), batch: (ind) => ind.batch(close) },
|
||||
BomarBands: { make: () => new wickra.BomarBands(4, 0.85), fields: ['upper', 'middle', 'lower'], step: (ind, i) => ind.update(close[i]), batch: (ind) => ind.batch(close) },
|
||||
MedianChannel: { make: () => new wickra.MedianChannel(5, 2.0), fields: ['upper', 'middle', 'lower'], step: (ind, i) => ind.update(close[i]), batch: (ind) => ind.batch(close) },
|
||||
ProjectionBands: { make: () => new wickra.ProjectionBands(3), fields: ['upper', 'middle', 'lower'], step: (ind, i) => ind.update(high[i], low[i]), batch: (ind) => ind.batch(high, low) },
|
||||
};
|
||||
|
||||
for (const [name, d] of Object.entries(multi)) {
|
||||
|
||||
Vendored
+65
@@ -199,6 +199,26 @@ export interface StandardErrorBandsValue {
|
||||
middle: number
|
||||
lower: number
|
||||
}
|
||||
export interface QuartileBandsValue {
|
||||
upper: number
|
||||
middle: number
|
||||
lower: number
|
||||
}
|
||||
export interface BomarBandsValue {
|
||||
upper: number
|
||||
middle: number
|
||||
lower: number
|
||||
}
|
||||
export interface MedianChannelValue {
|
||||
upper: number
|
||||
middle: number
|
||||
lower: number
|
||||
}
|
||||
export interface ProjectionBandsValue {
|
||||
upper: number
|
||||
middle: number
|
||||
lower: number
|
||||
}
|
||||
export interface DoubleBollingerValue {
|
||||
upperOuter: number
|
||||
upperInner: number
|
||||
@@ -1639,6 +1659,15 @@ export declare class VolatilityRatio {
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type ProjectionOscillatorNode = ProjectionOscillator
|
||||
export declare class ProjectionOscillator {
|
||||
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 StochNode = Stochastic
|
||||
export declare class Stochastic {
|
||||
constructor(kPeriod: number, dPeriod: number)
|
||||
@@ -2627,6 +2656,42 @@ export declare class StandardErrorBands {
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type QuartileBandsNode = QuartileBands
|
||||
export declare class QuartileBands {
|
||||
constructor(period: number)
|
||||
update(value: number): QuartileBandsValue | null
|
||||
batch(prices: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type BomarBandsNode = BomarBands
|
||||
export declare class BomarBands {
|
||||
constructor(period: number, coverage: number)
|
||||
update(value: number): BomarBandsValue | null
|
||||
batch(prices: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type MedianChannelNode = MedianChannel
|
||||
export declare class MedianChannel {
|
||||
constructor(period: number, multiplier: number)
|
||||
update(value: number): MedianChannelValue | null
|
||||
batch(prices: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type ProjectionBandsNode = ProjectionBands
|
||||
export declare class ProjectionBands {
|
||||
constructor(period: number)
|
||||
update(high: number, low: number): ProjectionBandsValue | null
|
||||
batch(high: Array<number>, low: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type DoubleBollingerNode = DoubleBollinger
|
||||
export declare class DoubleBollinger {
|
||||
constructor(period: number, kInner: number, kOuter: number)
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wickra-darwin-arm64",
|
||||
"version": "0.6.0",
|
||||
"version": "0.6.1",
|
||||
"description": "Native binding for wickra (macOS Apple Silicon). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||
"main": "wickra.darwin-arm64.node",
|
||||
"files": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wickra-darwin-x64",
|
||||
"version": "0.6.0",
|
||||
"version": "0.6.1",
|
||||
"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.6.0",
|
||||
"version": "0.6.1",
|
||||
"description": "Native binding for wickra (linux arm64 GNU). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||
"main": "wickra.linux-arm64-gnu.node",
|
||||
"files": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wickra-linux-x64-gnu",
|
||||
"version": "0.6.0",
|
||||
"version": "0.6.1",
|
||||
"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.6.0",
|
||||
"version": "0.6.1",
|
||||
"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.6.0",
|
||||
"version": "0.6.1",
|
||||
"description": "Native binding for wickra (Windows x64 MSVC). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||
"main": "wickra.win32-x64-msvc.node",
|
||||
"files": [
|
||||
|
||||
Generated
+20
-20
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "wickra",
|
||||
"version": "0.6.0",
|
||||
"version": "0.6.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "wickra",
|
||||
"version": "0.6.0",
|
||||
"version": "0.6.1",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"devDependencies": {
|
||||
"@napi-rs/cli": "^2.18.0"
|
||||
@@ -15,12 +15,12 @@
|
||||
"node": ">= 18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"wickra-darwin-arm64": "0.6.0",
|
||||
"wickra-darwin-x64": "0.6.0",
|
||||
"wickra-linux-arm64-gnu": "0.6.0",
|
||||
"wickra-linux-x64-gnu": "0.6.0",
|
||||
"wickra-win32-arm64-msvc": "0.6.0",
|
||||
"wickra-win32-x64-msvc": "0.6.0"
|
||||
"wickra-darwin-arm64": "0.6.1",
|
||||
"wickra-darwin-x64": "0.6.1",
|
||||
"wickra-linux-arm64-gnu": "0.6.1",
|
||||
"wickra-linux-x64-gnu": "0.6.1",
|
||||
"wickra-win32-arm64-msvc": "0.6.1",
|
||||
"wickra-win32-x64-msvc": "0.6.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/cli": {
|
||||
@@ -41,8 +41,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wickra-darwin-arm64": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/wickra-darwin-arm64/-/wickra-darwin-arm64-0.6.0.tgz",
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/wickra-darwin-arm64/-/wickra-darwin-arm64-0.6.1.tgz",
|
||||
"integrity": "sha512-4eZiBR/yGUdr4nzhEUFy2i69XgNx64iI2ax/LPamsThgylC0KpHOZKK19QzJ2d9KbK4C8nMjME5FLuR+4GNEwQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
@@ -57,8 +57,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wickra-darwin-x64": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/wickra-darwin-x64/-/wickra-darwin-x64-0.6.0.tgz",
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/wickra-darwin-x64/-/wickra-darwin-x64-0.6.1.tgz",
|
||||
"integrity": "sha512-6hf8zI3QPjTFp4zCpmgUwDvNtu6jHqNUHKD5e55POo0CgA52HkpyxSPtVm8TGTIZDI7kPjlbOdBM8CJ76mmXwA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
@@ -73,8 +73,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wickra-linux-arm64-gnu": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/wickra-linux-arm64-gnu/-/wickra-linux-arm64-gnu-0.6.0.tgz",
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/wickra-linux-arm64-gnu/-/wickra-linux-arm64-gnu-0.6.1.tgz",
|
||||
"integrity": "sha512-kSe6y0xBMSiqdPLXNjwop5WZdHtvdBNKSEBCwZ4hFq33p4apW25/wrlzv9/oDuyD4kuPabJEhCCnFOplh58CUg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
@@ -89,8 +89,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wickra-linux-x64-gnu": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/wickra-linux-x64-gnu/-/wickra-linux-x64-gnu-0.6.0.tgz",
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/wickra-linux-x64-gnu/-/wickra-linux-x64-gnu-0.6.1.tgz",
|
||||
"integrity": "sha512-tWBWS4qz7hxM4xnpFb59bhf6TaLwXq0Z3jEa/2l7r8PiHA94g8r8S53NRMiT+4yiL5hSWe/nUiC/YXdRrhEZ4g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
@@ -105,8 +105,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wickra-win32-arm64-msvc": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/wickra-win32-arm64-msvc/-/wickra-win32-arm64-msvc-0.6.0.tgz",
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/wickra-win32-arm64-msvc/-/wickra-win32-arm64-msvc-0.6.1.tgz",
|
||||
"integrity": "sha512-EXIckHxAtF75PUGDKRzXyqMe9ldP0JjSdu68WFN6iJfp+McYrGu6h40TEJlQ/oUEIoPqiZB/xhVyo/el5Lg7zw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
@@ -121,8 +121,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wickra-win32-x64-msvc": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/wickra-win32-x64-msvc/-/wickra-win32-x64-msvc-0.6.0.tgz",
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/wickra-win32-x64-msvc/-/wickra-win32-x64-msvc-0.6.1.tgz",
|
||||
"integrity": "sha512-Yfsqq1Xwp6hdxMyLze411vNdo7BDwI6+lPSe7A9XdqyPecNDbtKwYLpsal2r8EHbNzqM+R8XnuRtUaEQS5VlUQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wickra",
|
||||
"version": "0.6.0",
|
||||
"version": "0.6.1",
|
||||
"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.6.0",
|
||||
"wickra-linux-arm64-gnu": "0.6.0",
|
||||
"wickra-darwin-x64": "0.6.0",
|
||||
"wickra-darwin-arm64": "0.6.0",
|
||||
"wickra-win32-x64-msvc": "0.6.0",
|
||||
"wickra-win32-arm64-msvc": "0.6.0"
|
||||
"wickra-linux-x64-gnu": "0.6.1",
|
||||
"wickra-linux-arm64-gnu": "0.6.1",
|
||||
"wickra-darwin-x64": "0.6.1",
|
||||
"wickra-darwin-arm64": "0.6.1",
|
||||
"wickra-win32-x64-msvc": "0.6.1",
|
||||
"wickra-win32-arm64-msvc": "0.6.1"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "napi build --platform --release",
|
||||
|
||||
@@ -2869,6 +2869,59 @@ impl VolatilityRatioNode {
|
||||
}
|
||||
}
|
||||
|
||||
#[napi(js_name = "ProjectionOscillator")]
|
||||
pub struct ProjectionOscillatorNode {
|
||||
inner: wc::ProjectionOscillator,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl ProjectionOscillatorNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(period: u32) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::ProjectionOscillator::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(object)]
|
||||
pub struct StochValue {
|
||||
pub k: f64,
|
||||
@@ -8529,6 +8582,239 @@ impl StandardErrorBandsNode {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Quartile Bands ----------
|
||||
|
||||
#[napi(object)]
|
||||
pub struct QuartileBandsValue {
|
||||
pub upper: f64,
|
||||
pub middle: f64,
|
||||
pub lower: f64,
|
||||
}
|
||||
|
||||
#[napi(js_name = "QuartileBands")]
|
||||
pub struct QuartileBandsNode {
|
||||
inner: wc::QuartileBands,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl QuartileBandsNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(period: u32) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::QuartileBands::new(period as usize).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(&mut self, value: f64) -> Option<QuartileBandsValue> {
|
||||
self.inner.update(value).map(|o| QuartileBandsValue {
|
||||
upper: o.upper,
|
||||
middle: o.middle,
|
||||
lower: o.lower,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(&mut self, prices: Vec<f64>) -> Vec<f64> {
|
||||
let mut out = vec![f64::NAN; prices.len() * 3];
|
||||
for (i, p) in prices.iter().enumerate() {
|
||||
if let Some(o) = self.inner.update(*p) {
|
||||
out[i * 3] = o.upper;
|
||||
out[i * 3 + 1] = o.middle;
|
||||
out[i * 3 + 2] = o.lower;
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Bomar Bands ----------
|
||||
|
||||
#[napi(object)]
|
||||
pub struct BomarBandsValue {
|
||||
pub upper: f64,
|
||||
pub middle: f64,
|
||||
pub lower: f64,
|
||||
}
|
||||
|
||||
#[napi(js_name = "BomarBands")]
|
||||
pub struct BomarBandsNode {
|
||||
inner: wc::BomarBands,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl BomarBandsNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(period: u32, coverage: f64) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::BomarBands::new(period as usize, coverage).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(&mut self, value: f64) -> Option<BomarBandsValue> {
|
||||
self.inner.update(value).map(|o| BomarBandsValue {
|
||||
upper: o.upper,
|
||||
middle: o.middle,
|
||||
lower: o.lower,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(&mut self, prices: Vec<f64>) -> Vec<f64> {
|
||||
let mut out = vec![f64::NAN; prices.len() * 3];
|
||||
for (i, p) in prices.iter().enumerate() {
|
||||
if let Some(o) = self.inner.update(*p) {
|
||||
out[i * 3] = o.upper;
|
||||
out[i * 3 + 1] = o.middle;
|
||||
out[i * 3 + 2] = o.lower;
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Median Channel ----------
|
||||
|
||||
#[napi(object)]
|
||||
pub struct MedianChannelValue {
|
||||
pub upper: f64,
|
||||
pub middle: f64,
|
||||
pub lower: f64,
|
||||
}
|
||||
|
||||
#[napi(js_name = "MedianChannel")]
|
||||
pub struct MedianChannelNode {
|
||||
inner: wc::MedianChannel,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl MedianChannelNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(period: u32, multiplier: f64) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::MedianChannel::new(period as usize, multiplier).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(&mut self, value: f64) -> Option<MedianChannelValue> {
|
||||
self.inner.update(value).map(|o| MedianChannelValue {
|
||||
upper: o.upper,
|
||||
middle: o.middle,
|
||||
lower: o.lower,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(&mut self, prices: Vec<f64>) -> Vec<f64> {
|
||||
let mut out = vec![f64::NAN; prices.len() * 3];
|
||||
for (i, p) in prices.iter().enumerate() {
|
||||
if let Some(o) = self.inner.update(*p) {
|
||||
out[i * 3] = o.upper;
|
||||
out[i * 3 + 1] = o.middle;
|
||||
out[i * 3 + 2] = o.lower;
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Projection Bands ----------
|
||||
|
||||
#[napi(object)]
|
||||
pub struct ProjectionBandsValue {
|
||||
pub upper: f64,
|
||||
pub middle: f64,
|
||||
pub lower: f64,
|
||||
}
|
||||
|
||||
#[napi(js_name = "ProjectionBands")]
|
||||
pub struct ProjectionBandsNode {
|
||||
inner: wc::ProjectionBands,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl ProjectionBandsNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(period: u32) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::ProjectionBands::new(period as usize).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(&mut self, high: f64, low: f64) -> napi::Result<Option<ProjectionBandsValue>> {
|
||||
Ok(self
|
||||
.inner
|
||||
.update(cnd(high, low, low, 0.0)?)
|
||||
.map(|o| ProjectionBandsValue {
|
||||
upper: o.upper,
|
||||
middle: o.middle,
|
||||
lower: o.lower,
|
||||
}))
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(&mut self, high: Vec<f64>, low: Vec<f64>) -> napi::Result<Vec<f64>> {
|
||||
if high.len() != low.len() {
|
||||
return Err(NapiError::from_reason(
|
||||
"high and low must be equal length".to_string(),
|
||||
));
|
||||
}
|
||||
let n = high.len();
|
||||
let mut out = vec![f64::NAN; n * 3];
|
||||
for i in 0..n {
|
||||
if let Some(o) = self.inner.update(cnd(high[i], low[i], low[i], 0.0)?) {
|
||||
out[i * 3] = o.upper;
|
||||
out[i * 3 + 1] = o.middle;
|
||||
out[i * 3 + 2] = 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
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Double Bollinger ----------
|
||||
|
||||
#[napi(object)]
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "maturin"
|
||||
|
||||
[project]
|
||||
name = "wickra"
|
||||
version = "0.6.0"
|
||||
version = "0.6.1"
|
||||
description = "Streaming-first technical indicators: incremental, fast, install-free."
|
||||
readme = "README.md"
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
@@ -25,6 +25,7 @@ from __future__ import annotations
|
||||
|
||||
from ._wickra import (
|
||||
__version__,
|
||||
ProjectionOscillator,
|
||||
VolatilityCone,
|
||||
VolatilityRatio,
|
||||
BipowerVariation,
|
||||
@@ -263,6 +264,10 @@ from ._wickra import (
|
||||
MAMA,
|
||||
FAMA,
|
||||
# Bands & Channels
|
||||
ProjectionBands,
|
||||
MedianChannel,
|
||||
BomarBands,
|
||||
QuartileBands,
|
||||
MaEnvelope,
|
||||
AccelerationBands,
|
||||
StarcBands,
|
||||
@@ -482,6 +487,7 @@ from ._wickra import (
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"ProjectionOscillator",
|
||||
"VolatilityCone",
|
||||
"VolatilityRatio",
|
||||
"BipowerVariation",
|
||||
@@ -721,6 +727,10 @@ __all__ = [
|
||||
"MAMA",
|
||||
"FAMA",
|
||||
# Bands & Channels
|
||||
"ProjectionBands",
|
||||
"MedianChannel",
|
||||
"BomarBands",
|
||||
"QuartileBands",
|
||||
"MaEnvelope",
|
||||
"AccelerationBands",
|
||||
"StarcBands",
|
||||
|
||||
@@ -3559,6 +3559,78 @@ impl PyVolatilityRatio {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== ProjectionOscillator ==============================
|
||||
|
||||
#[pyclass(
|
||||
name = "ProjectionOscillator",
|
||||
module = "wickra._wickra",
|
||||
skip_from_py_object
|
||||
)]
|
||||
#[derive(Clone)]
|
||||
struct PyProjectionOscillator {
|
||||
inner: wc::ProjectionOscillator,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyProjectionOscillator {
|
||||
#[new]
|
||||
#[pyo3(signature = (period=14))]
|
||||
fn new(period: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::ProjectionOscillator::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!("ProjectionOscillator(period={})", self.inner.period())
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Stochastic ==============================
|
||||
|
||||
#[pyclass(name = "IMI", module = "wickra._wickra", skip_from_py_object)]
|
||||
@@ -11309,6 +11381,233 @@ impl PyStandardErrorBands {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Quartile Bands ==============================
|
||||
|
||||
#[pyclass(name = "QuartileBands", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyQuartileBands {
|
||||
inner: wc::QuartileBands,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyQuartileBands {
|
||||
#[new]
|
||||
#[pyo3(signature = (period=20))]
|
||||
fn new(period: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::QuartileBands::new(period).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
fn update(&mut self, value: f64) -> Option<(f64, f64, f64)> {
|
||||
self.inner
|
||||
.update(value)
|
||||
.map(|o| (o.upper, o.middle, o.lower))
|
||||
}
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
prices: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray2<f64>>> {
|
||||
let slice = prices
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let n = slice.len();
|
||||
let mut out = vec![f64::NAN; n * 3];
|
||||
for (i, p) in slice.iter().enumerate() {
|
||||
if let Some(o) = self.inner.update(*p) {
|
||||
out[i * 3] = o.upper;
|
||||
out[i * 3 + 1] = o.middle;
|
||||
out[i * 3 + 2] = o.lower;
|
||||
}
|
||||
}
|
||||
Ok(numpy::ndarray::Array2::from_shape_vec((n, 3), 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()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Bomar Bands ==============================
|
||||
|
||||
#[pyclass(name = "BomarBands", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyBomarBands {
|
||||
inner: wc::BomarBands,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyBomarBands {
|
||||
#[new]
|
||||
#[pyo3(signature = (period=20, coverage=0.85))]
|
||||
fn new(period: usize, coverage: f64) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::BomarBands::new(period, coverage).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
fn update(&mut self, value: f64) -> Option<(f64, f64, f64)> {
|
||||
self.inner
|
||||
.update(value)
|
||||
.map(|o| (o.upper, o.middle, o.lower))
|
||||
}
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
prices: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray2<f64>>> {
|
||||
let slice = prices
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let n = slice.len();
|
||||
let mut out = vec![f64::NAN; n * 3];
|
||||
for (i, p) in slice.iter().enumerate() {
|
||||
if let Some(o) = self.inner.update(*p) {
|
||||
out[i * 3] = o.upper;
|
||||
out[i * 3 + 1] = o.middle;
|
||||
out[i * 3 + 2] = o.lower;
|
||||
}
|
||||
}
|
||||
Ok(numpy::ndarray::Array2::from_shape_vec((n, 3), 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()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Median Channel ==============================
|
||||
|
||||
#[pyclass(name = "MedianChannel", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyMedianChannel {
|
||||
inner: wc::MedianChannel,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyMedianChannel {
|
||||
#[new]
|
||||
#[pyo3(signature = (period=20, multiplier=2.0))]
|
||||
fn new(period: usize, multiplier: f64) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::MedianChannel::new(period, multiplier).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
fn update(&mut self, value: f64) -> Option<(f64, f64, f64)> {
|
||||
self.inner
|
||||
.update(value)
|
||||
.map(|o| (o.upper, o.middle, o.lower))
|
||||
}
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
prices: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray2<f64>>> {
|
||||
let slice = prices
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let n = slice.len();
|
||||
let mut out = vec![f64::NAN; n * 3];
|
||||
for (i, p) in slice.iter().enumerate() {
|
||||
if let Some(o) = self.inner.update(*p) {
|
||||
out[i * 3] = o.upper;
|
||||
out[i * 3 + 1] = o.middle;
|
||||
out[i * 3 + 2] = o.lower;
|
||||
}
|
||||
}
|
||||
Ok(numpy::ndarray::Array2::from_shape_vec((n, 3), 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()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Projection Bands ==============================
|
||||
|
||||
#[pyclass(
|
||||
name = "ProjectionBands",
|
||||
module = "wickra._wickra",
|
||||
skip_from_py_object
|
||||
)]
|
||||
#[derive(Clone)]
|
||||
struct PyProjectionBands {
|
||||
inner: wc::ProjectionBands,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyProjectionBands {
|
||||
#[new]
|
||||
#[pyo3(signature = (period=14))]
|
||||
fn new(period: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::ProjectionBands::new(period).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult<Option<(f64, f64, f64)>> {
|
||||
let c = extract_candle(candle)?;
|
||||
Ok(self.inner.update(c).map(|o| (o.upper, o.middle, o.lower)))
|
||||
}
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: 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))?;
|
||||
if h.len() != l.len() {
|
||||
return Err(PyValueError::new_err("high and low must be equal length"));
|
||||
}
|
||||
let n = h.len();
|
||||
let mut out = vec![f64::NAN; n * 3];
|
||||
for i in 0..n {
|
||||
let candle = wc::Candle::new(h[i], h[i], l[i], l[i], 0.0, 0).map_err(map_err)?;
|
||||
if let Some(o) = self.inner.update(candle) {
|
||||
out[i * 3] = o.upper;
|
||||
out[i * 3 + 1] = o.middle;
|
||||
out[i * 3 + 2] = o.lower;
|
||||
}
|
||||
}
|
||||
Ok(numpy::ndarray::Array2::from_shape_vec((n, 3), 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()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Double Bollinger ==============================
|
||||
|
||||
#[pyclass(
|
||||
@@ -21633,6 +21932,10 @@ fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_class::<PyHurstChannel>()?;
|
||||
m.add_class::<PyLinRegChannel>()?;
|
||||
m.add_class::<PyStandardErrorBands>()?;
|
||||
m.add_class::<PyQuartileBands>()?;
|
||||
m.add_class::<PyBomarBands>()?;
|
||||
m.add_class::<PyMedianChannel>()?;
|
||||
m.add_class::<PyProjectionBands>()?;
|
||||
m.add_class::<PyDoubleBollinger>()?;
|
||||
m.add_class::<PyTtmSqueeze>()?;
|
||||
m.add_class::<PyFractalChaosBands>()?;
|
||||
@@ -21934,5 +22237,6 @@ fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_class::<PyGarch11>()?;
|
||||
m.add_class::<PyVolatilityOfVolatility>()?;
|
||||
m.add_class::<PyVolatilityCone>()?;
|
||||
m.add_class::<PyProjectionOscillator>()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -173,6 +173,9 @@ SCALAR = [
|
||||
# Family 05 band/channel indicators with scalar input and multi-output.
|
||||
# `cols` is the expected number of band columns from `batch`.
|
||||
SCALAR_MULTI = {
|
||||
"MedianChannel": (lambda: ta.MedianChannel(5, 2.0), 3),
|
||||
"BomarBands": (lambda: ta.BomarBands(4, 0.85), 3),
|
||||
"QuartileBands": (lambda: ta.QuartileBands(4), 3),
|
||||
"Qqe": (lambda: ta.QQE(14, 5, 4.236), 2),
|
||||
"MaEnvelope": (lambda: ta.MaEnvelope(20, 0.025), 3),
|
||||
"LinRegChannel": (lambda: ta.LinRegChannel(20, 2.0), 3),
|
||||
@@ -365,6 +368,7 @@ def test_relative_strength_streaming_matches_batch():
|
||||
# 6-tuple candle; the batch helper takes only the columns it needs.
|
||||
|
||||
CANDLE_SCALAR = {
|
||||
"ProjectionOscillator": (lambda: ta.ProjectionOscillator(14), lambda ind, h, l, c, v: ind.batch(h, l, c)),
|
||||
"VolatilityRatio": (lambda: ta.VolatilityRatio(14), lambda ind, h, l, c, v: ind.batch(h, l, c)),
|
||||
"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)),
|
||||
@@ -904,6 +908,11 @@ def test_candle_scalar_streaming_matches_batch(name, ohlcv):
|
||||
# --- Candle-input, multi-output indicators --------------------------------
|
||||
|
||||
MULTI = {
|
||||
"ProjectionBands": (
|
||||
lambda: ta.ProjectionBands(3),
|
||||
lambda ind, h, l, c, v: ind.batch(h, l),
|
||||
3,
|
||||
),
|
||||
"VolatilityCone": (
|
||||
lambda: ta.VolatilityCone(20, 60),
|
||||
lambda ind, h, l, c, v: ind.batch(h, l, c),
|
||||
@@ -2918,6 +2927,46 @@ def test_garch11_reference():
|
||||
def test_volatility_cone_reference():
|
||||
t = ta.VolatilityCone(20, 60)
|
||||
|
||||
|
||||
def test_quartile_bands_reference():
|
||||
t = ta.QuartileBands(4)
|
||||
assert t.update(40.0) is None
|
||||
assert t.update(30.0) is None
|
||||
assert t.update(20.0) is None
|
||||
assert t.update(10.0) == pytest.approx((32.5, 25.0, 17.5))
|
||||
|
||||
|
||||
def test_bomar_bands_reference():
|
||||
t = ta.BomarBands(4, 0.85)
|
||||
assert t.update(100.0) is None
|
||||
assert t.update(102.0) is None
|
||||
assert t.update(98.0) is None
|
||||
assert t.update(104.0) == pytest.approx((104.0, 101.0, 98.0))
|
||||
|
||||
|
||||
def test_median_channel_reference():
|
||||
t = ta.MedianChannel(5, 2.0)
|
||||
assert t.update(1.0) is None
|
||||
assert t.update(2.0) is None
|
||||
assert t.update(3.0) is None
|
||||
assert t.update(4.0) is None
|
||||
assert t.update(5.0) == pytest.approx((5.0, 3.0, 1.0))
|
||||
|
||||
|
||||
def test_projection_bands_reference():
|
||||
t = ta.ProjectionBands(3)
|
||||
assert t.update((8.0, 10.0, 8.0, 9.0, 1.0, 0)) is None
|
||||
assert t.update((9.0, 12.0, 9.0, 11.0, 1.0, 1)) is None
|
||||
assert t.update((10.0, 11.0, 10.0, 11.0, 1.0, 2)) == pytest.approx((12.5, 11.25, 10.0))
|
||||
|
||||
|
||||
def test_projection_oscillator_reference():
|
||||
# Same window as ProjectionBands: upper 12.5, lower 10; close 11 -> 40.
|
||||
t = ta.ProjectionOscillator(3)
|
||||
assert t.update((8.0, 10.0, 8.0, 9.0, 1.0, 0)) is None
|
||||
assert t.update((9.0, 12.0, 9.0, 11.0, 1.0, 1)) is None
|
||||
assert t.update((10.0, 11.0, 10.0, 11.0, 1.0, 2)) == pytest.approx(40.0)
|
||||
|
||||
# --- Lifecycle ------------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
@@ -2514,6 +2514,44 @@ impl WasmVolatilityRatio {
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = ProjectionOscillator)]
|
||||
pub struct WasmProjectionOscillator {
|
||||
inner: wc::ProjectionOscillator,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = ProjectionOscillator)]
|
||||
impl WasmProjectionOscillator {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(period: usize) -> Result<WasmProjectionOscillator, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::ProjectionOscillator::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 = Stochastic)]
|
||||
pub struct WasmStoch {
|
||||
inner: wc::Stochastic,
|
||||
@@ -5844,6 +5882,219 @@ impl WasmStandardErrorBands {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Quartile Bands (scalar input, 3 outputs) ----------
|
||||
|
||||
#[wasm_bindgen(js_name = QuartileBands)]
|
||||
pub struct WasmQuartileBands {
|
||||
inner: wc::QuartileBands,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = QuartileBands)]
|
||||
impl WasmQuartileBands {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(period: usize) -> Result<WasmQuartileBands, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::QuartileBands::new(period).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
pub fn update(&mut self, value: f64) -> JsValue {
|
||||
match self.inner.update(value) {
|
||||
Some(o) => {
|
||||
let obj = Object::new();
|
||||
Reflect::set(&obj, &"upper".into(), &o.upper.into()).ok();
|
||||
Reflect::set(&obj, &"middle".into(), &o.middle.into()).ok();
|
||||
Reflect::set(&obj, &"lower".into(), &o.lower.into()).ok();
|
||||
obj.into()
|
||||
}
|
||||
None => JsValue::NULL,
|
||||
}
|
||||
}
|
||||
pub fn batch(&mut self, prices: &[f64]) -> Float64Array {
|
||||
let n = prices.len();
|
||||
let mut out = vec![f64::NAN; n * 3];
|
||||
for (i, p) in prices.iter().enumerate() {
|
||||
if let Some(o) = self.inner.update(*p) {
|
||||
out[i * 3] = o.upper;
|
||||
out[i * 3 + 1] = o.middle;
|
||||
out[i * 3 + 2] = o.lower;
|
||||
}
|
||||
}
|
||||
Float64Array::from(out.as_slice())
|
||||
}
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
#[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()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Bomar Bands (scalar input, 3 outputs) ----------
|
||||
|
||||
#[wasm_bindgen(js_name = BomarBands)]
|
||||
pub struct WasmBomarBands {
|
||||
inner: wc::BomarBands,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = BomarBands)]
|
||||
impl WasmBomarBands {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(period: usize, coverage: f64) -> Result<WasmBomarBands, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::BomarBands::new(period, coverage).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
pub fn update(&mut self, value: f64) -> JsValue {
|
||||
match self.inner.update(value) {
|
||||
Some(o) => {
|
||||
let obj = Object::new();
|
||||
Reflect::set(&obj, &"upper".into(), &o.upper.into()).ok();
|
||||
Reflect::set(&obj, &"middle".into(), &o.middle.into()).ok();
|
||||
Reflect::set(&obj, &"lower".into(), &o.lower.into()).ok();
|
||||
obj.into()
|
||||
}
|
||||
None => JsValue::NULL,
|
||||
}
|
||||
}
|
||||
pub fn batch(&mut self, prices: &[f64]) -> Float64Array {
|
||||
let n = prices.len();
|
||||
let mut out = vec![f64::NAN; n * 3];
|
||||
for (i, p) in prices.iter().enumerate() {
|
||||
if let Some(o) = self.inner.update(*p) {
|
||||
out[i * 3] = o.upper;
|
||||
out[i * 3 + 1] = o.middle;
|
||||
out[i * 3 + 2] = o.lower;
|
||||
}
|
||||
}
|
||||
Float64Array::from(out.as_slice())
|
||||
}
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
#[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()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Median Channel (scalar input, 3 outputs) ----------
|
||||
|
||||
#[wasm_bindgen(js_name = MedianChannel)]
|
||||
pub struct WasmMedianChannel {
|
||||
inner: wc::MedianChannel,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = MedianChannel)]
|
||||
impl WasmMedianChannel {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(period: usize, multiplier: f64) -> Result<WasmMedianChannel, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::MedianChannel::new(period, multiplier).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
pub fn update(&mut self, value: f64) -> JsValue {
|
||||
match self.inner.update(value) {
|
||||
Some(o) => {
|
||||
let obj = Object::new();
|
||||
Reflect::set(&obj, &"upper".into(), &o.upper.into()).ok();
|
||||
Reflect::set(&obj, &"middle".into(), &o.middle.into()).ok();
|
||||
Reflect::set(&obj, &"lower".into(), &o.lower.into()).ok();
|
||||
obj.into()
|
||||
}
|
||||
None => JsValue::NULL,
|
||||
}
|
||||
}
|
||||
pub fn batch(&mut self, prices: &[f64]) -> Float64Array {
|
||||
let n = prices.len();
|
||||
let mut out = vec![f64::NAN; n * 3];
|
||||
for (i, p) in prices.iter().enumerate() {
|
||||
if let Some(o) = self.inner.update(*p) {
|
||||
out[i * 3] = o.upper;
|
||||
out[i * 3 + 1] = o.middle;
|
||||
out[i * 3 + 2] = o.lower;
|
||||
}
|
||||
}
|
||||
Float64Array::from(out.as_slice())
|
||||
}
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
#[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()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Projection Bands (high/low input, 3 outputs) ----------
|
||||
|
||||
#[wasm_bindgen(js_name = ProjectionBands)]
|
||||
pub struct WasmProjectionBands {
|
||||
inner: wc::ProjectionBands,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = ProjectionBands)]
|
||||
impl WasmProjectionBands {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(period: usize) -> Result<WasmProjectionBands, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::ProjectionBands::new(period).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
pub fn update(&mut self, high: f64, low: f64) -> Result<JsValue, JsError> {
|
||||
let candle = make_candle(high, low, low, 0.0)?;
|
||||
match self.inner.update(candle) {
|
||||
Some(o) => {
|
||||
let obj = Object::new();
|
||||
Reflect::set(&obj, &"upper".into(), &o.upper.into()).ok();
|
||||
Reflect::set(&obj, &"middle".into(), &o.middle.into()).ok();
|
||||
Reflect::set(&obj, &"lower".into(), &o.lower.into()).ok();
|
||||
Ok(obj.into())
|
||||
}
|
||||
None => Ok(JsValue::NULL),
|
||||
}
|
||||
}
|
||||
pub fn batch(&mut self, high: &[f64], low: &[f64]) -> Result<Float64Array, JsError> {
|
||||
if high.len() != low.len() {
|
||||
return Err(JsError::new("high and low must be equal length"));
|
||||
}
|
||||
let n = high.len();
|
||||
let mut out = vec![f64::NAN; n * 3];
|
||||
for i in 0..n {
|
||||
let candle = make_candle(high[i], low[i], low[i], 0.0)?;
|
||||
if let Some(o) = self.inner.update(candle) {
|
||||
out[i * 3] = o.upper;
|
||||
out[i * 3 + 1] = o.middle;
|
||||
out[i * 3 + 2] = o.lower;
|
||||
}
|
||||
}
|
||||
Ok(Float64Array::from(out.as_slice()))
|
||||
}
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
#[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()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Double Bollinger (scalar input, 5 outputs) ----------
|
||||
|
||||
#[wasm_bindgen(js_name = DoubleBollinger)]
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
//! Bomar Bands — adaptive percentage bands that contain a target fraction of
|
||||
//! recent price.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::indicators::rolling_quantile::quantile_sorted;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Bomar Bands output.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct BomarBandsOutput {
|
||||
/// Upper band: `middle + |middle| · p`.
|
||||
pub upper: f64,
|
||||
/// Middle line: the simple moving average over the window.
|
||||
pub middle: f64,
|
||||
/// Lower band: `middle − |middle| · p`.
|
||||
pub lower: f64,
|
||||
}
|
||||
|
||||
/// Bomar Bands: percentage bands whose width adapts so that a fixed `coverage`
|
||||
/// fraction of recent closes falls inside them.
|
||||
///
|
||||
/// The Bomar Bands predate Bollinger Bands; John Bollinger cites them as an
|
||||
/// inspiration — percentage bands around a moving average, with the percentage
|
||||
/// tuned so a fixed share (classically ~85%) of price stayed within. Wickra
|
||||
/// realises that idea deterministically: the half-width is the `coverage`
|
||||
/// quantile of the relative deviations from the midline, so by construction
|
||||
/// `coverage` of the window's closes lie inside the bands.
|
||||
///
|
||||
/// ```text
|
||||
/// middle = SMA(close, period)
|
||||
/// dev_i = | close_i / middle − 1 | // relative distance from midline
|
||||
/// p = coverage-quantile of { dev_i } // type-7 interpolation
|
||||
/// upper = middle + |middle| · p
|
||||
/// lower = middle − |middle| · p
|
||||
/// ```
|
||||
///
|
||||
/// Unlike the fixed-percentage [`MaEnvelope`](crate::MaEnvelope), the offset
|
||||
/// here is data-driven: the bands widen in turbulent regimes and tighten in
|
||||
/// quiet ones without a volatility input. Unlike Bollinger Bands, the width is
|
||||
/// an order statistic of the actual deviations rather than a multiple of the
|
||||
/// standard deviation, so it is unaffected by the shape of the tails beyond the
|
||||
/// `coverage` rank. When the midline is zero the relative deviation is
|
||||
/// undefined and the bands collapse onto the midline.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{BomarBands, Indicator};
|
||||
///
|
||||
/// let mut indicator = BomarBands::new(20, 0.85).unwrap();
|
||||
/// let mut last = None;
|
||||
/// for i in 0..40 {
|
||||
/// last = indicator.update(100.0 + f64::from(i % 7));
|
||||
/// }
|
||||
/// assert!(last.is_some());
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BomarBands {
|
||||
period: usize,
|
||||
coverage: f64,
|
||||
window: VecDeque<f64>,
|
||||
scratch: Vec<f64>,
|
||||
}
|
||||
|
||||
impl BomarBands {
|
||||
/// Construct new Bomar Bands.
|
||||
///
|
||||
/// `coverage` is the target fraction of closes to contain, in `(0.0, 1.0]`.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns [`Error::PeriodZero`] if `period == 0`, or
|
||||
/// [`Error::InvalidParameter`] if `coverage` is not a finite value in
|
||||
/// `(0.0, 1.0]`.
|
||||
pub fn new(period: usize, coverage: f64) -> Result<Self> {
|
||||
if period == 0 {
|
||||
return Err(Error::PeriodZero);
|
||||
}
|
||||
if !coverage.is_finite() || coverage <= 0.0 || coverage > 1.0 {
|
||||
return Err(Error::InvalidParameter {
|
||||
message: "bomar bands coverage must be a finite value in (0.0, 1.0]",
|
||||
});
|
||||
}
|
||||
Ok(Self {
|
||||
period,
|
||||
coverage,
|
||||
window: VecDeque::with_capacity(period),
|
||||
scratch: Vec::with_capacity(period),
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured period.
|
||||
pub const fn period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
|
||||
/// Configured coverage fraction.
|
||||
pub const fn coverage(&self) -> f64 {
|
||||
self.coverage
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for BomarBands {
|
||||
type Input = f64;
|
||||
type Output = BomarBandsOutput;
|
||||
|
||||
fn update(&mut self, value: f64) -> Option<BomarBandsOutput> {
|
||||
if self.window.len() == self.period {
|
||||
self.window.pop_front();
|
||||
}
|
||||
self.window.push_back(value);
|
||||
if self.window.len() < self.period {
|
||||
return None;
|
||||
}
|
||||
let sum: f64 = self.window.iter().sum();
|
||||
let middle = sum / (self.period as f64);
|
||||
let denom = middle.abs();
|
||||
|
||||
self.scratch.clear();
|
||||
for &v in &self.window {
|
||||
let dev = if denom == 0.0 {
|
||||
0.0
|
||||
} else {
|
||||
((v - middle) / denom).abs()
|
||||
};
|
||||
self.scratch.push(dev);
|
||||
}
|
||||
self.scratch.sort_by(f64::total_cmp);
|
||||
let p = quantile_sorted(&self.scratch, self.coverage);
|
||||
let offset = denom * p;
|
||||
|
||||
Some(BomarBandsOutput {
|
||||
upper: middle + offset,
|
||||
middle,
|
||||
lower: middle - offset,
|
||||
})
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.window.clear();
|
||||
self.scratch.clear();
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.window.len() == self.period
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"BomarBands"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
#[test]
|
||||
fn rejects_zero_period() {
|
||||
assert!(matches!(BomarBands::new(0, 0.85), Err(Error::PeriodZero)));
|
||||
assert!(BomarBands::new(1, 0.85).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_out_of_range_coverage() {
|
||||
assert!(matches!(
|
||||
BomarBands::new(20, 0.0),
|
||||
Err(Error::InvalidParameter { .. })
|
||||
));
|
||||
assert!(matches!(
|
||||
BomarBands::new(20, 1.1),
|
||||
Err(Error::InvalidParameter { .. })
|
||||
));
|
||||
assert!(matches!(
|
||||
BomarBands::new(20, -0.5),
|
||||
Err(Error::InvalidParameter { .. })
|
||||
));
|
||||
assert!(matches!(
|
||||
BomarBands::new(20, f64::NAN),
|
||||
Err(Error::InvalidParameter { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let bb = BomarBands::new(20, 0.85).unwrap();
|
||||
assert_eq!(bb.period(), 20);
|
||||
assert_relative_eq!(bb.coverage(), 0.85, epsilon = 1e-12);
|
||||
assert_eq!(bb.warmup_period(), 20);
|
||||
assert_eq!(bb.name(), "BomarBands");
|
||||
assert!(!bb.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn warms_up_then_emits() {
|
||||
let mut bb = BomarBands::new(4, 0.85).unwrap();
|
||||
assert!(bb.update(100.0).is_none());
|
||||
assert!(bb.update(102.0).is_none());
|
||||
assert!(bb.update(98.0).is_none());
|
||||
assert!(bb.update(104.0).is_some());
|
||||
assert!(bb.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn known_bands() {
|
||||
// mean=101; |dev| = {1,1,3,3}/101; coverage 0.85 quantile -> 3/101.
|
||||
// offset = 101 * 3/101 = 3 -> upper 104, lower 98.
|
||||
let mut bb = BomarBands::new(4, 0.85).unwrap();
|
||||
let out = bb.batch(&[100.0, 102.0, 98.0, 104.0]);
|
||||
let last = out[3].unwrap();
|
||||
assert_relative_eq!(last.middle, 101.0, epsilon = 1e-9);
|
||||
assert_relative_eq!(last.upper, 104.0, epsilon = 1e-9);
|
||||
assert_relative_eq!(last.lower, 98.0, epsilon = 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_midline_collapses_bands() {
|
||||
// Window mean exactly zero -> relative deviation undefined -> collapse.
|
||||
let mut bb = BomarBands::new(2, 0.85).unwrap();
|
||||
let out = bb.batch(&[3.0, -3.0]);
|
||||
let last = out[1].unwrap();
|
||||
assert_relative_eq!(last.middle, 0.0, epsilon = 1e-12);
|
||||
assert_relative_eq!(last.upper, 0.0, epsilon = 1e-12);
|
||||
assert_relative_eq!(last.lower, 0.0, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rolling_window_evicts_oldest() {
|
||||
// Eight values through a period-4 window: only the last four survive,
|
||||
// reproducing the `known_bands` window.
|
||||
let mut bb = BomarBands::new(4, 0.85).unwrap();
|
||||
let out = bb.batch(&[50.0, 50.0, 50.0, 50.0, 100.0, 102.0, 98.0, 104.0]);
|
||||
let last = out[7].unwrap();
|
||||
assert_relative_eq!(last.middle, 101.0, epsilon = 1e-9);
|
||||
assert_relative_eq!(last.upper, 104.0, epsilon = 1e-9);
|
||||
assert_relative_eq!(last.lower, 98.0, epsilon = 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut bb = BomarBands::new(4, 0.85).unwrap();
|
||||
for v in [100.0, 102.0, 98.0, 104.0] {
|
||||
bb.update(v);
|
||||
}
|
||||
assert!(bb.is_ready());
|
||||
bb.reset();
|
||||
assert!(!bb.is_ready());
|
||||
assert!(bb.update(100.0).is_none());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
//! Median Channel — a robust median ± MAD envelope.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::indicators::rolling_quantile::quantile_sorted;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Median Channel output.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct MedianChannelOutput {
|
||||
/// Upper band: `median + multiplier · MAD`.
|
||||
pub upper: f64,
|
||||
/// Middle line: the rolling median.
|
||||
pub middle: f64,
|
||||
/// Lower band: `median − multiplier · MAD`.
|
||||
pub lower: f64,
|
||||
}
|
||||
|
||||
/// Median Channel: a robust analogue of Bollinger Bands built from the rolling
|
||||
/// median and the median absolute deviation (MAD).
|
||||
///
|
||||
/// ```text
|
||||
/// middle = median(close, period)
|
||||
/// MAD = median( | close_i − middle | )
|
||||
/// upper = middle + multiplier · MAD
|
||||
/// lower = middle − multiplier · MAD
|
||||
/// ```
|
||||
///
|
||||
/// Where [`BollingerBands`](crate::BollingerBands) centre on the mean and scale
|
||||
/// by the standard deviation — both of which a single spike can drag
|
||||
/// arbitrarily far — the Median Channel uses two order statistics. The
|
||||
/// breakdown point of the median and MAD is 50%: up to half the window can be
|
||||
/// contaminated before the centre or width is materially distorted. That makes
|
||||
/// the channel well suited to noisy, gap-prone, or fat-tailed series where
|
||||
/// Bollinger Bands flare on every outlier. Both quantiles use the type-7
|
||||
/// interpolation shared with [`RollingQuantile`](crate::RollingQuantile).
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Indicator, MedianChannel};
|
||||
///
|
||||
/// let mut indicator = MedianChannel::new(20, 2.0).unwrap();
|
||||
/// let mut last = None;
|
||||
/// for i in 0..40 {
|
||||
/// last = indicator.update(100.0 + f64::from(i % 5));
|
||||
/// }
|
||||
/// assert!(last.is_some());
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MedianChannel {
|
||||
period: usize,
|
||||
multiplier: f64,
|
||||
window: VecDeque<f64>,
|
||||
scratch: Vec<f64>,
|
||||
deviations: Vec<f64>,
|
||||
}
|
||||
|
||||
impl MedianChannel {
|
||||
/// Construct a new Median Channel.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns [`Error::PeriodZero`] if `period == 0`, or
|
||||
/// [`Error::NonPositiveMultiplier`] if `multiplier` is not strictly
|
||||
/// positive and finite.
|
||||
pub fn new(period: usize, multiplier: f64) -> Result<Self> {
|
||||
if period == 0 {
|
||||
return Err(Error::PeriodZero);
|
||||
}
|
||||
if !multiplier.is_finite() || multiplier <= 0.0 {
|
||||
return Err(Error::NonPositiveMultiplier);
|
||||
}
|
||||
Ok(Self {
|
||||
period,
|
||||
multiplier,
|
||||
window: VecDeque::with_capacity(period),
|
||||
scratch: Vec::with_capacity(period),
|
||||
deviations: Vec::with_capacity(period),
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured period.
|
||||
pub const fn period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
|
||||
/// Configured multiplier.
|
||||
pub const fn multiplier(&self) -> f64 {
|
||||
self.multiplier
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for MedianChannel {
|
||||
type Input = f64;
|
||||
type Output = MedianChannelOutput;
|
||||
|
||||
fn update(&mut self, value: f64) -> Option<MedianChannelOutput> {
|
||||
if self.window.len() == self.period {
|
||||
self.window.pop_front();
|
||||
}
|
||||
self.window.push_back(value);
|
||||
if self.window.len() < self.period {
|
||||
return None;
|
||||
}
|
||||
self.scratch.clear();
|
||||
self.scratch.extend(self.window.iter().copied());
|
||||
self.scratch.sort_by(f64::total_cmp);
|
||||
let median = quantile_sorted(&self.scratch, 0.5);
|
||||
|
||||
self.deviations.clear();
|
||||
for &v in &self.window {
|
||||
self.deviations.push((v - median).abs());
|
||||
}
|
||||
self.deviations.sort_by(f64::total_cmp);
|
||||
let mad = quantile_sorted(&self.deviations, 0.5);
|
||||
let offset = self.multiplier * mad;
|
||||
|
||||
Some(MedianChannelOutput {
|
||||
upper: median + offset,
|
||||
middle: median,
|
||||
lower: median - offset,
|
||||
})
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.window.clear();
|
||||
self.scratch.clear();
|
||||
self.deviations.clear();
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.window.len() == self.period
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"MedianChannel"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
#[test]
|
||||
fn rejects_zero_period() {
|
||||
assert!(matches!(MedianChannel::new(0, 2.0), Err(Error::PeriodZero)));
|
||||
assert!(MedianChannel::new(1, 2.0).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_non_positive_multiplier() {
|
||||
assert!(matches!(
|
||||
MedianChannel::new(20, 0.0),
|
||||
Err(Error::NonPositiveMultiplier)
|
||||
));
|
||||
assert!(matches!(
|
||||
MedianChannel::new(20, -1.0),
|
||||
Err(Error::NonPositiveMultiplier)
|
||||
));
|
||||
assert!(matches!(
|
||||
MedianChannel::new(20, f64::NAN),
|
||||
Err(Error::NonPositiveMultiplier)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let mc = MedianChannel::new(20, 2.0).unwrap();
|
||||
assert_eq!(mc.period(), 20);
|
||||
assert_relative_eq!(mc.multiplier(), 2.0, epsilon = 1e-12);
|
||||
assert_eq!(mc.warmup_period(), 20);
|
||||
assert_eq!(mc.name(), "MedianChannel");
|
||||
assert!(!mc.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn warms_up_then_emits() {
|
||||
let mut mc = MedianChannel::new(5, 2.0).unwrap();
|
||||
for v in [1.0, 2.0, 3.0, 4.0] {
|
||||
assert!(mc.update(v).is_none());
|
||||
}
|
||||
assert!(mc.update(5.0).is_some());
|
||||
assert!(mc.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn known_channel() {
|
||||
// [1,2,3,4,5]: median 3; |dev| sorted [0,1,1,2,2] -> MAD 1.
|
||||
// upper = 3 + 2*1 = 5; lower = 3 - 2*1 = 1.
|
||||
let mut mc = MedianChannel::new(5, 2.0).unwrap();
|
||||
let out = mc.batch(&[1.0, 2.0, 3.0, 4.0, 5.0]);
|
||||
let last = out[4].unwrap();
|
||||
assert_relative_eq!(last.middle, 3.0, epsilon = 1e-12);
|
||||
assert_relative_eq!(last.upper, 5.0, epsilon = 1e-12);
|
||||
assert_relative_eq!(last.lower, 1.0, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn robust_to_outlier() {
|
||||
// Replacing the last value with a huge spike leaves the median centre
|
||||
// unchanged (still the middle order statistic).
|
||||
let mut mc = MedianChannel::new(5, 2.0).unwrap();
|
||||
let out = mc.batch(&[1.0, 2.0, 3.0, 4.0, 1_000.0]);
|
||||
assert_relative_eq!(out[4].unwrap().middle, 3.0, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rolling_window_evicts_oldest() {
|
||||
// Ten values through a period-5 window: only the last five survive,
|
||||
// reproducing the `known_channel` window.
|
||||
let mut mc = MedianChannel::new(5, 2.0).unwrap();
|
||||
let out = mc.batch(&[10.0, 10.0, 10.0, 10.0, 10.0, 1.0, 2.0, 3.0, 4.0, 5.0]);
|
||||
let last = out[9].unwrap();
|
||||
assert_relative_eq!(last.middle, 3.0, epsilon = 1e-12);
|
||||
assert_relative_eq!(last.upper, 5.0, epsilon = 1e-12);
|
||||
assert_relative_eq!(last.lower, 1.0, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut mc = MedianChannel::new(5, 2.0).unwrap();
|
||||
for v in [1.0, 2.0, 3.0, 4.0, 5.0] {
|
||||
mc.update(v);
|
||||
}
|
||||
assert!(mc.is_ready());
|
||||
mc.reset();
|
||||
assert!(!mc.is_ready());
|
||||
assert!(mc.update(1.0).is_none());
|
||||
}
|
||||
}
|
||||
@@ -52,6 +52,7 @@ mod bipower_variation;
|
||||
mod body_size_pct;
|
||||
mod bollinger;
|
||||
mod bollinger_bandwidth;
|
||||
mod bomar_bands;
|
||||
mod breadth_thrust;
|
||||
mod breakaway;
|
||||
mod bullish_percent_index;
|
||||
@@ -229,6 +230,7 @@ mod mcclellan_oscillator;
|
||||
mod mcclellan_summation_index;
|
||||
mod mcginley_dynamic;
|
||||
mod median_absolute_deviation;
|
||||
mod median_channel;
|
||||
mod median_ma;
|
||||
mod median_price;
|
||||
mod mfi;
|
||||
@@ -276,10 +278,13 @@ mod polarized_fractal_efficiency;
|
||||
mod ppo;
|
||||
mod ppo_histogram;
|
||||
mod profit_factor;
|
||||
mod projection_bands;
|
||||
mod projection_oscillator;
|
||||
mod psar;
|
||||
mod pvi;
|
||||
mod qqe;
|
||||
mod qstick;
|
||||
mod quartile_bands;
|
||||
mod quoted_spread;
|
||||
mod r_squared;
|
||||
mod realized_spread;
|
||||
@@ -481,6 +486,7 @@ pub use bipower_variation::BipowerVariation;
|
||||
pub use body_size_pct::BodySizePct;
|
||||
pub use bollinger::{BollingerBands, BollingerOutput};
|
||||
pub use bollinger_bandwidth::BollingerBandwidth;
|
||||
pub use bomar_bands::{BomarBands, BomarBandsOutput};
|
||||
pub use breadth_thrust::BreadthThrust;
|
||||
pub use breakaway::Breakaway;
|
||||
pub use bullish_percent_index::BullishPercentIndex;
|
||||
@@ -658,6 +664,7 @@ pub use mcclellan_oscillator::McClellanOscillator;
|
||||
pub use mcclellan_summation_index::McClellanSummationIndex;
|
||||
pub use mcginley_dynamic::McGinleyDynamic;
|
||||
pub use median_absolute_deviation::MedianAbsoluteDeviation;
|
||||
pub use median_channel::{MedianChannel, MedianChannelOutput};
|
||||
pub use median_ma::MedianMa;
|
||||
pub use median_price::MedianPrice;
|
||||
pub use mfi::Mfi;
|
||||
@@ -705,10 +712,13 @@ pub use polarized_fractal_efficiency::PolarizedFractalEfficiency;
|
||||
pub use ppo::Ppo;
|
||||
pub use ppo_histogram::PpoHistogram;
|
||||
pub use profit_factor::ProfitFactor;
|
||||
pub use projection_bands::{ProjectionBands, ProjectionBandsOutput};
|
||||
pub use projection_oscillator::ProjectionOscillator;
|
||||
pub use psar::Psar;
|
||||
pub use pvi::Pvi;
|
||||
pub use qqe::{Qqe, QqeOutput};
|
||||
pub use qstick::Qstick;
|
||||
pub use quartile_bands::{QuartileBands, QuartileBandsOutput};
|
||||
pub use quoted_spread::QuotedSpread;
|
||||
pub use r_squared::RSquared;
|
||||
pub use realized_spread::RealizedSpread;
|
||||
@@ -1040,6 +1050,11 @@ pub const FAMILIES: &[(&str, &[&str])] = &[
|
||||
"TtmSqueeze",
|
||||
"FractalChaosBands",
|
||||
"VwapStdDevBands",
|
||||
"QuartileBands",
|
||||
"BomarBands",
|
||||
"MedianChannel",
|
||||
"ProjectionBands",
|
||||
"ProjectionOscillator",
|
||||
],
|
||||
),
|
||||
(
|
||||
@@ -1441,6 +1456,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, 429, "FAMILIES total drifted from indicator count");
|
||||
assert_eq!(total, 434, "FAMILIES total drifted from indicator count");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,253 @@
|
||||
//! Projection Bands (Mel Widner) — a high/low linear-regression projection
|
||||
//! envelope.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Projection Bands output.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct ProjectionBandsOutput {
|
||||
/// Upper band: the maximum forward-projected high in the window.
|
||||
pub upper: f64,
|
||||
/// Middle line: the midpoint of the upper and lower bands.
|
||||
pub middle: f64,
|
||||
/// Lower band: the minimum forward-projected low in the window.
|
||||
pub lower: f64,
|
||||
}
|
||||
|
||||
/// Projection Bands: forward-projected high/low envelope.
|
||||
///
|
||||
/// Mel Widner ("Projection Bands and the Projection Oscillator", *Technical
|
||||
/// Analysis of Stocks & Commodities*, May 1995) fits a separate linear
|
||||
/// regression to the highs and to the lows over the last `period` bars, then
|
||||
/// slides every bar's high and low forward to the current bar along its own
|
||||
/// slope. The upper band is the maximum of the projected highs, the lower band
|
||||
/// the minimum of the projected lows:
|
||||
///
|
||||
/// ```text
|
||||
/// slope_h = OLS slope of (x, high) over the window
|
||||
/// slope_l = OLS slope of (x, low) over the window
|
||||
/// // bar i (0 = oldest, period-1 = newest) is (period-1-i) bars in the past
|
||||
/// upper = max over i of [ high_i + slope_h · (period-1-i) ]
|
||||
/// lower = min over i of [ low_i + slope_l · (period-1-i) ]
|
||||
/// middle = (upper + lower) / 2
|
||||
/// ```
|
||||
///
|
||||
/// Unlike [`LinRegChannel`](crate::LinRegChannel) and
|
||||
/// [`StandardErrorBands`](crate::StandardErrorBands) — which wrap a single
|
||||
/// close-regression endpoint by a dispersion statistic — Projection Bands are
|
||||
/// built from the *extremes*: the envelope adapts to the trend's slope yet
|
||||
/// always contains every projected high and low, so by construction price never
|
||||
/// pierces the bands within the window. A flat slope reduces the bands to the
|
||||
/// rolling highest-high / lowest-low (a Donchian channel); a steep slope tilts
|
||||
/// the whole envelope with the trend.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Candle, Indicator, ProjectionBands};
|
||||
///
|
||||
/// let mut indicator = ProjectionBands::new(14).unwrap();
|
||||
/// let mut last = None;
|
||||
/// for i in 0..30 {
|
||||
/// let base = 100.0 + f64::from(i);
|
||||
/// let candle =
|
||||
/// Candle::new(base, base + 2.0, base - 2.0, base + 1.0, 10.0, i64::from(i)).unwrap();
|
||||
/// last = indicator.update(candle);
|
||||
/// }
|
||||
/// assert!(last.is_some());
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProjectionBands {
|
||||
period: usize,
|
||||
highs: VecDeque<f64>,
|
||||
lows: VecDeque<f64>,
|
||||
sum_x: f64,
|
||||
sum_xx: f64,
|
||||
}
|
||||
|
||||
impl ProjectionBands {
|
||||
/// Construct new Projection Bands.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns [`Error::InvalidPeriod`] if `period < 2` (a regression slope
|
||||
/// needs at least two points).
|
||||
pub fn new(period: usize) -> Result<Self> {
|
||||
if period < 2 {
|
||||
return Err(Error::InvalidPeriod {
|
||||
message: "projection bands need period >= 2",
|
||||
});
|
||||
}
|
||||
let n = period as f64;
|
||||
Ok(Self {
|
||||
period,
|
||||
highs: VecDeque::with_capacity(period),
|
||||
lows: VecDeque::with_capacity(period),
|
||||
sum_x: n * (n - 1.0) / 2.0,
|
||||
sum_xx: (n - 1.0) * n * (2.0 * n - 1.0) / 6.0,
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured period.
|
||||
pub const fn period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
|
||||
/// OLS slope of `(0..period, values)` over the live window.
|
||||
fn slope(&self, values: &VecDeque<f64>) -> f64 {
|
||||
let n = self.period as f64;
|
||||
let mut sum_y = 0.0;
|
||||
let mut sum_xy = 0.0;
|
||||
for (i, &y) in values.iter().enumerate() {
|
||||
sum_y += y;
|
||||
sum_xy += (i as f64) * y;
|
||||
}
|
||||
let denom = n * self.sum_xx - self.sum_x * self.sum_x;
|
||||
(n * sum_xy - self.sum_x * sum_y) / denom
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for ProjectionBands {
|
||||
type Input = Candle;
|
||||
type Output = ProjectionBandsOutput;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<ProjectionBandsOutput> {
|
||||
if self.highs.len() == self.period {
|
||||
self.highs.pop_front();
|
||||
self.lows.pop_front();
|
||||
}
|
||||
self.highs.push_back(candle.high);
|
||||
self.lows.push_back(candle.low);
|
||||
if self.highs.len() < self.period {
|
||||
return None;
|
||||
}
|
||||
|
||||
let slope_h = self.slope(&self.highs);
|
||||
let slope_l = self.slope(&self.lows);
|
||||
let last = (self.period - 1) as f64;
|
||||
|
||||
let mut upper = f64::NEG_INFINITY;
|
||||
let mut lower = f64::INFINITY;
|
||||
for (i, (&high, &low)) in self.highs.iter().zip(self.lows.iter()).enumerate() {
|
||||
let forward = last - (i as f64);
|
||||
let projected_high = high + slope_h * forward;
|
||||
let projected_low = low + slope_l * forward;
|
||||
if projected_high > upper {
|
||||
upper = projected_high;
|
||||
}
|
||||
if projected_low < lower {
|
||||
lower = projected_low;
|
||||
}
|
||||
}
|
||||
|
||||
Some(ProjectionBandsOutput {
|
||||
upper,
|
||||
middle: f64::midpoint(upper, lower),
|
||||
lower,
|
||||
})
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.highs.clear();
|
||||
self.lows.clear();
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.highs.len() == self.period
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"ProjectionBands"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
fn candle(high: f64, low: f64, close: f64, ts: i64) -> Candle {
|
||||
Candle::new(low, high, low, close, 10.0, ts).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_period_below_two() {
|
||||
assert!(matches!(
|
||||
ProjectionBands::new(0),
|
||||
Err(Error::InvalidPeriod { .. })
|
||||
));
|
||||
assert!(matches!(
|
||||
ProjectionBands::new(1),
|
||||
Err(Error::InvalidPeriod { .. })
|
||||
));
|
||||
assert!(ProjectionBands::new(2).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let pb = ProjectionBands::new(14).unwrap();
|
||||
assert_eq!(pb.period(), 14);
|
||||
assert_eq!(pb.warmup_period(), 14);
|
||||
assert_eq!(pb.name(), "ProjectionBands");
|
||||
assert!(!pb.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn warms_up_then_emits() {
|
||||
let mut pb = ProjectionBands::new(3).unwrap();
|
||||
assert!(pb.update(candle(10.0, 8.0, 9.0, 0)).is_none());
|
||||
assert!(pb.update(candle(12.0, 9.0, 11.0, 1)).is_none());
|
||||
assert!(pb.update(candle(11.0, 10.0, 11.0, 2)).is_some());
|
||||
assert!(pb.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn known_projection() {
|
||||
// highs 10,12,11 -> slope_h = 0.5; projected = 11, 12.5, 11 -> upper 12.5
|
||||
// lows 8, 9,10 -> slope_l = 1.0; projected = 10, 10, 10 -> lower 10
|
||||
let mut pb = ProjectionBands::new(3).unwrap();
|
||||
pb.update(candle(10.0, 8.0, 9.0, 0));
|
||||
pb.update(candle(12.0, 9.0, 11.0, 1));
|
||||
let out = pb.update(candle(11.0, 10.0, 11.0, 2)).unwrap();
|
||||
assert_relative_eq!(out.upper, 12.5, epsilon = 1e-9);
|
||||
assert_relative_eq!(out.lower, 10.0, epsilon = 1e-9);
|
||||
assert_relative_eq!(out.middle, 11.25, epsilon = 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn perfect_trend_pins_bands_to_current_extremes() {
|
||||
// High_i and Low_i both rise by exactly 1 per bar: every projected high
|
||||
// collapses onto the current high, every projected low onto the current
|
||||
// low.
|
||||
let mut pb = ProjectionBands::new(5).unwrap();
|
||||
let mut last = None;
|
||||
for i in 0..10 {
|
||||
let high = 100.0 + f64::from(i);
|
||||
let low = 95.0 + f64::from(i);
|
||||
last = pb.update(candle(high, low, high, i64::from(i)));
|
||||
}
|
||||
let out = last.unwrap();
|
||||
assert_relative_eq!(out.upper, 109.0, epsilon = 1e-9);
|
||||
assert_relative_eq!(out.lower, 104.0, epsilon = 1e-9);
|
||||
assert_relative_eq!(out.middle, 106.5, epsilon = 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut pb = ProjectionBands::new(3).unwrap();
|
||||
pb.update(candle(10.0, 8.0, 9.0, 0));
|
||||
pb.update(candle(12.0, 9.0, 11.0, 1));
|
||||
pb.update(candle(11.0, 10.0, 11.0, 2));
|
||||
assert!(pb.is_ready());
|
||||
pb.reset();
|
||||
assert!(!pb.is_ready());
|
||||
assert!(pb.update(candle(10.0, 8.0, 9.0, 3)).is_none());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
//! Projection Oscillator (Mel Widner) — the close's position inside the
|
||||
//! [`ProjectionBands`](crate::ProjectionBands).
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::indicators::projection_bands::ProjectionBands;
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Projection Oscillator: where the close sits inside the projection bands,
|
||||
/// scaled to `0..100`.
|
||||
///
|
||||
/// The companion to [`ProjectionBands`](crate::ProjectionBands) from Mel
|
||||
/// Widner's May 1995 *Stocks & Commodities* article. It maps the close onto the
|
||||
/// `[lower, upper]` projection envelope:
|
||||
///
|
||||
/// ```text
|
||||
/// PO = 100 · (close − lower) / (upper − lower)
|
||||
/// ```
|
||||
///
|
||||
/// `PO = 0` means the close is sitting on the lower band, `PO = 100` on the
|
||||
/// upper band, and `PO = 50` at the midline. Because the bands by construction
|
||||
/// bracket every projected high and low, the close almost always falls inside
|
||||
/// them and the oscillator stays in `0..100` — readings near the extremes flag
|
||||
/// an overbought/oversold position *relative to the trend-tilted channel*
|
||||
/// rather than to a horizontal level. When the bands collapse (a zero-range
|
||||
/// window, `upper == lower`) the position is undefined and the oscillator
|
||||
/// returns the neutral `50.0`.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Candle, Indicator, ProjectionOscillator};
|
||||
///
|
||||
/// let mut indicator = ProjectionOscillator::new(14).unwrap();
|
||||
/// let mut last = None;
|
||||
/// for i in 0..30 {
|
||||
/// let base = 100.0 + f64::from(i);
|
||||
/// let candle =
|
||||
/// Candle::new(base, base + 2.0, base - 2.0, base + 1.0, 10.0, i64::from(i)).unwrap();
|
||||
/// last = indicator.update(candle);
|
||||
/// }
|
||||
/// assert!(last.is_some());
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProjectionOscillator {
|
||||
bands: ProjectionBands,
|
||||
}
|
||||
|
||||
impl ProjectionOscillator {
|
||||
/// Construct a new Projection Oscillator.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns [`Error::InvalidPeriod`](crate::Error::InvalidPeriod) if
|
||||
/// `period < 2`.
|
||||
pub fn new(period: usize) -> Result<Self> {
|
||||
Ok(Self {
|
||||
bands: ProjectionBands::new(period)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured period.
|
||||
pub const fn period(&self) -> usize {
|
||||
self.bands.period()
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for ProjectionOscillator {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
let bands = self.bands.update(candle)?;
|
||||
let width = bands.upper - bands.lower;
|
||||
if width == 0.0 {
|
||||
return Some(50.0);
|
||||
}
|
||||
Some(100.0 * (candle.close - bands.lower) / width)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.bands.reset();
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.bands.warmup_period()
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.bands.is_ready()
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"ProjectionOscillator"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::error::Error;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
fn candle(high: f64, low: f64, close: f64, ts: i64) -> Candle {
|
||||
Candle::new(low, high, low, close, 10.0, ts).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_period_below_two() {
|
||||
assert!(matches!(
|
||||
ProjectionOscillator::new(1),
|
||||
Err(Error::InvalidPeriod { .. })
|
||||
));
|
||||
assert!(ProjectionOscillator::new(2).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let po = ProjectionOscillator::new(14).unwrap();
|
||||
assert_eq!(po.period(), 14);
|
||||
assert_eq!(po.warmup_period(), 14);
|
||||
assert_eq!(po.name(), "ProjectionOscillator");
|
||||
assert!(!po.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn warms_up_then_emits() {
|
||||
let mut po = ProjectionOscillator::new(3).unwrap();
|
||||
assert!(po.update(candle(10.0, 8.0, 9.0, 0)).is_none());
|
||||
assert!(po.update(candle(12.0, 9.0, 11.0, 1)).is_none());
|
||||
assert!(po.update(candle(11.0, 10.0, 11.0, 2)).is_some());
|
||||
assert!(po.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn known_position() {
|
||||
// Same window as ProjectionBands::known_projection: upper 12.5, lower 10.
|
||||
// close 11 -> 100 * (11 - 10) / (12.5 - 10) = 40.
|
||||
let mut po = ProjectionOscillator::new(3).unwrap();
|
||||
po.update(candle(10.0, 8.0, 9.0, 0));
|
||||
po.update(candle(12.0, 9.0, 11.0, 1));
|
||||
let out = po.update(candle(11.0, 10.0, 11.0, 2)).unwrap();
|
||||
assert_relative_eq!(out, 40.0, epsilon = 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collapsed_bands_return_neutral() {
|
||||
// Zero-range, perfectly trending candles: upper == lower every bar.
|
||||
let mut po = ProjectionOscillator::new(3).unwrap();
|
||||
let mut last = None;
|
||||
for i in 0..6 {
|
||||
let v = 100.0 + f64::from(i);
|
||||
last = po.update(candle(v, v, v, i64::from(i)));
|
||||
}
|
||||
assert_relative_eq!(last.unwrap(), 50.0, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut po = ProjectionOscillator::new(3).unwrap();
|
||||
po.update(candle(10.0, 8.0, 9.0, 0));
|
||||
po.update(candle(12.0, 9.0, 11.0, 1));
|
||||
po.update(candle(11.0, 10.0, 11.0, 2));
|
||||
assert!(po.is_ready());
|
||||
po.reset();
|
||||
assert!(!po.is_ready());
|
||||
assert!(po.update(candle(10.0, 8.0, 9.0, 3)).is_none());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
//! Quartile Bands — rolling 25th / 50th / 75th percentile envelope.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::indicators::rolling_quantile::quantile_sorted;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Quartile Bands output.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct QuartileBandsOutput {
|
||||
/// Upper band: the rolling third quartile (75th percentile, `Q3`).
|
||||
pub upper: f64,
|
||||
/// Middle line: the rolling median (50th percentile, `Q2`).
|
||||
pub middle: f64,
|
||||
/// Lower band: the rolling first quartile (25th percentile, `Q1`).
|
||||
pub lower: f64,
|
||||
}
|
||||
|
||||
/// Quartile Bands: a distribution-based envelope drawn at the rolling quartiles.
|
||||
///
|
||||
/// ```text
|
||||
/// lower = Q1 = 25th percentile of the last `period` values
|
||||
/// middle = Q2 = 50th percentile (median)
|
||||
/// upper = Q3 = 75th percentile
|
||||
/// ```
|
||||
///
|
||||
/// Quantiles use the type-7 (`NumPy`/`R-7`) linear interpolation shared with
|
||||
/// [`RollingQuantile`](crate::RollingQuantile). Where Bollinger Bands assume an
|
||||
/// approximately normal distribution and size the envelope by the mean and
|
||||
/// standard deviation, Quartile Bands are fully **non-parametric**: the band
|
||||
/// edges are order statistics, so a single outlier shifts at most one rank
|
||||
/// rather than inflating the whole width, and the inter-quartile span between
|
||||
/// the bands is exactly the [`RollingIqr`](crate::RollingIqr). The middle line
|
||||
/// is the robust median rather than the mean, so it is unmoved by spikes.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Indicator, QuartileBands};
|
||||
///
|
||||
/// let mut indicator = QuartileBands::new(20).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 QuartileBands {
|
||||
period: usize,
|
||||
window: VecDeque<f64>,
|
||||
scratch: Vec<f64>,
|
||||
}
|
||||
|
||||
impl QuartileBands {
|
||||
/// Construct new Quartile Bands.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns [`Error::PeriodZero`] if `period == 0`.
|
||||
pub fn new(period: usize) -> Result<Self> {
|
||||
if period == 0 {
|
||||
return Err(Error::PeriodZero);
|
||||
}
|
||||
Ok(Self {
|
||||
period,
|
||||
window: VecDeque::with_capacity(period),
|
||||
scratch: Vec::with_capacity(period),
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured period.
|
||||
pub const fn period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for QuartileBands {
|
||||
type Input = f64;
|
||||
type Output = QuartileBandsOutput;
|
||||
|
||||
fn update(&mut self, value: f64) -> Option<QuartileBandsOutput> {
|
||||
if self.window.len() == self.period {
|
||||
self.window.pop_front();
|
||||
}
|
||||
self.window.push_back(value);
|
||||
if self.window.len() < self.period {
|
||||
return None;
|
||||
}
|
||||
self.scratch.clear();
|
||||
self.scratch.extend(self.window.iter().copied());
|
||||
self.scratch.sort_by(f64::total_cmp);
|
||||
Some(QuartileBandsOutput {
|
||||
upper: quantile_sorted(&self.scratch, 0.75),
|
||||
middle: quantile_sorted(&self.scratch, 0.5),
|
||||
lower: quantile_sorted(&self.scratch, 0.25),
|
||||
})
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.window.clear();
|
||||
self.scratch.clear();
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.window.len() == self.period
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"QuartileBands"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
#[test]
|
||||
fn rejects_zero_period() {
|
||||
assert!(matches!(QuartileBands::new(0), Err(Error::PeriodZero)));
|
||||
assert!(QuartileBands::new(1).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let qb = QuartileBands::new(20).unwrap();
|
||||
assert_eq!(qb.period(), 20);
|
||||
assert_eq!(qb.warmup_period(), 20);
|
||||
assert_eq!(qb.name(), "QuartileBands");
|
||||
assert!(!qb.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn warms_up_then_emits() {
|
||||
let mut qb = QuartileBands::new(4).unwrap();
|
||||
assert!(qb.update(10.0).is_none());
|
||||
assert!(qb.update(20.0).is_none());
|
||||
assert!(qb.update(30.0).is_none());
|
||||
assert!(qb.update(40.0).is_some());
|
||||
assert!(qb.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn known_quartiles() {
|
||||
// sorted [10,20,30,40]:
|
||||
// Q1 h=(4-1)*0.25=0.75 -> 10 + 0.75*10 = 17.5
|
||||
// Q2 h=1.5 -> 20 + 0.5*10 = 25.0
|
||||
// Q3 h=2.25 -> 30 + 0.25*10 = 32.5
|
||||
let mut qb = QuartileBands::new(4).unwrap();
|
||||
let out = qb.batch(&[40.0, 30.0, 20.0, 10.0]);
|
||||
let last = out[3].unwrap();
|
||||
assert_relative_eq!(last.lower, 17.5, epsilon = 1e-9);
|
||||
assert_relative_eq!(last.middle, 25.0, epsilon = 1e-9);
|
||||
assert_relative_eq!(last.upper, 32.5, epsilon = 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn median_robust_to_outlier() {
|
||||
// A single spike shifts the mean a lot but the median by at most one rank.
|
||||
let mut qb = QuartileBands::new(5).unwrap();
|
||||
let out = qb.batch(&[1.0, 2.0, 3.0, 4.0, 1000.0]);
|
||||
assert_relative_eq!(out[4].unwrap().middle, 3.0, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rolling_window_evicts_oldest() {
|
||||
// Eight values through a period-4 window: only the last four survive,
|
||||
// reproducing the `known_quartiles` window.
|
||||
let mut qb = QuartileBands::new(4).unwrap();
|
||||
let out = qb.batch(&[1.0, 2.0, 3.0, 4.0, 40.0, 30.0, 20.0, 10.0]);
|
||||
let last = out[7].unwrap();
|
||||
assert_relative_eq!(last.lower, 17.5, epsilon = 1e-9);
|
||||
assert_relative_eq!(last.middle, 25.0, epsilon = 1e-9);
|
||||
assert_relative_eq!(last.upper, 32.5, epsilon = 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut qb = QuartileBands::new(4).unwrap();
|
||||
for v in [10.0, 20.0, 30.0, 40.0] {
|
||||
qb.update(v);
|
||||
}
|
||||
assert!(qb.is_ready());
|
||||
qb.reset();
|
||||
assert!(!qb.is_ready());
|
||||
assert!(qb.update(10.0).is_none());
|
||||
}
|
||||
}
|
||||
@@ -64,20 +64,20 @@ pub use indicators::{
|
||||
AutoFibOutput, Autocorrelation, AverageDailyRange, AverageDrawdown, AvgPrice,
|
||||
AwesomeOscillator, AwesomeOscillatorHistogram, BalanceOfPower, Bat, BeltHold, Beta,
|
||||
BetaNeutralSpread, BipowerVariation, BodySizePct, BollingerBands, BollingerBandwidth,
|
||||
BollingerOutput, BreadthThrust, Breakaway, BullishPercentIndex, Butterfly, CalendarSpread,
|
||||
CalmarRatio, Camarilla, CamarillaPivotsOutput, Cci, CenterOfGravity, Cfo, ChaikinMoneyFlow,
|
||||
ChaikinOscillator, ChaikinVolatility, ChandeKrollStop, ChandeKrollStopOutput, ChandelierExit,
|
||||
ChandelierExitOutput, ChoppinessIndex, ClassicPivots, ClassicPivotsOutput, CloseVsOpen,
|
||||
ClosingMarubozu, Cmo, CoefficientOfVariation, Cointegration, CointegrationOutput,
|
||||
ConcealingBabySwallow, ConditionalValueAtRisk, ConnorsRsi, Coppock, Counterattack, Crab,
|
||||
CumulativeVolumeDelta, CumulativeVolumeIndex, CupAndHandle, CyberneticCycle, Cypher,
|
||||
DayOfWeekProfile, DayOfWeekProfileOutput, Decycler, DecyclerOscillator, Dema, DemandIndex,
|
||||
DemarkPivots, DemarkPivotsOutput, DepthSlope, DerivativeOscillator, DetrendedStdDev,
|
||||
DisparityIndex, DistanceSsd, Doji, DojiStar, Donchian, DonchianOutput, DonchianStop,
|
||||
DonchianStopOutput, DoubleBollinger, DoubleBollingerOutput, DoubleTopBottom,
|
||||
DownsideGapThreeMethods, Dpo, DragonflyDoji, DrawdownDuration, Dx, DynamicMomentumIndex,
|
||||
EaseOfMovement, EffectiveSpread, EhlersStochastic, Ehma, ElderImpulse, ElderRay,
|
||||
ElderRayOutput, Ema, EmpiricalModeDecomposition, Engulfing, EveningDojiStar, Evwma,
|
||||
BollingerOutput, BomarBands, BomarBandsOutput, BreadthThrust, Breakaway, BullishPercentIndex,
|
||||
Butterfly, CalendarSpread, CalmarRatio, Camarilla, CamarillaPivotsOutput, Cci, CenterOfGravity,
|
||||
Cfo, ChaikinMoneyFlow, ChaikinOscillator, ChaikinVolatility, ChandeKrollStop,
|
||||
ChandeKrollStopOutput, ChandelierExit, ChandelierExitOutput, ChoppinessIndex, ClassicPivots,
|
||||
ClassicPivotsOutput, CloseVsOpen, ClosingMarubozu, Cmo, CoefficientOfVariation, Cointegration,
|
||||
CointegrationOutput, ConcealingBabySwallow, ConditionalValueAtRisk, ConnorsRsi, Coppock,
|
||||
Counterattack, Crab, CumulativeVolumeDelta, CumulativeVolumeIndex, CupAndHandle,
|
||||
CyberneticCycle, Cypher, DayOfWeekProfile, DayOfWeekProfileOutput, Decycler,
|
||||
DecyclerOscillator, Dema, DemandIndex, DemarkPivots, DemarkPivotsOutput, DepthSlope,
|
||||
DerivativeOscillator, DetrendedStdDev, DisparityIndex, DistanceSsd, Doji, DojiStar, Donchian,
|
||||
DonchianOutput, DonchianStop, DonchianStopOutput, DoubleBollinger, DoubleBollingerOutput,
|
||||
DoubleTopBottom, DownsideGapThreeMethods, Dpo, DragonflyDoji, DrawdownDuration, Dx,
|
||||
DynamicMomentumIndex, EaseOfMovement, EffectiveSpread, EhlersStochastic, Ehma, ElderImpulse,
|
||||
ElderRay, ElderRayOutput, Ema, EmpiricalModeDecomposition, Engulfing, EveningDojiStar, Evwma,
|
||||
EwmaVolatility, Expectancy, FallingThreeMethods, Fama, FibArcs, FibArcsOutput, FibChannel,
|
||||
FibChannelOutput, FibConfluence, FibConfluenceOutput, FibExtension, FibExtensionOutput, FibFan,
|
||||
FibFanOutput, FibProjection, FibProjectionOutput, FibRetracement, FibRetracementOutput,
|
||||
@@ -102,27 +102,29 @@ pub use indicators::{
|
||||
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,
|
||||
McClellanSummationIndex, McGinleyDynamic, MedianAbsoluteDeviation, MedianChannel,
|
||||
MedianChannelOutput, 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, ProjectionBands, ProjectionBandsOutput, ProjectionOscillator, Psar, Pvi, Qqe,
|
||||
QqeOutput, Qstick, QuartileBands, QuartileBandsOutput, 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,
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ That includes:
|
||||
[Python](https://docs.wickra.org/Quickstart-Python),
|
||||
[Node](https://docs.wickra.org/Quickstart-Node), and
|
||||
[WASM](https://docs.wickra.org/Quickstart-WASM).
|
||||
- A per-indicator deep dive for every one of the **429 indicators** across
|
||||
- A per-indicator deep dive for every one of the **434 indicators** across
|
||||
the sixteen families (Moving Averages, Momentum Oscillators, Trend &
|
||||
Directional, Price Oscillators, Volatility & Bands, Bands & Channels,
|
||||
Trailing Stops, Volume, Price Statistics, Ehlers / Cycle DSP, Pivots &
|
||||
|
||||
Generated
+7
-7
@@ -17,7 +17,7 @@
|
||||
},
|
||||
"../../bindings/node": {
|
||||
"name": "wickra",
|
||||
"version": "0.6.0",
|
||||
"version": "0.6.1",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"devDependencies": {
|
||||
"@napi-rs/cli": "^2.18.0"
|
||||
@@ -26,12 +26,12 @@
|
||||
"node": ">= 18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"wickra-darwin-arm64": "0.6.0",
|
||||
"wickra-darwin-x64": "0.6.0",
|
||||
"wickra-linux-arm64-gnu": "0.6.0",
|
||||
"wickra-linux-x64-gnu": "0.6.0",
|
||||
"wickra-win32-arm64-msvc": "0.6.0",
|
||||
"wickra-win32-x64-msvc": "0.6.0"
|
||||
"wickra-darwin-arm64": "0.6.1",
|
||||
"wickra-darwin-x64": "0.6.1",
|
||||
"wickra-linux-arm64-gnu": "0.6.1",
|
||||
"wickra-linux-x64-gnu": "0.6.1",
|
||||
"wickra-win32-arm64-msvc": "0.6.1",
|
||||
"wickra-win32-x64-msvc": "0.6.1"
|
||||
}
|
||||
},
|
||||
"node_modules/wickra": {
|
||||
|
||||
@@ -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, BipowerVariation, 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, EwmaVolatility, Expectancy, Fama, FisherRsi, FisherTransform, Frama, GainLossRatio, Garch11, 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, VolatilityOfVolatility, WavePm, WinRate, Wma, ZScore, ZeroLagMacd, Zlema, T3};
|
||||
use wickra_core::{AdaptiveCycle, AdaptiveLaguerreFilter, Alma, AnchoredRsi, Apo, Autocorrelation, AverageDrawdown, BatchExt, Beta, BipowerVariation, BollingerBands, BomarBands, CalmarRatio, CenterOfGravity, Cfo, Cmo, CoefficientOfVariation, ConditionalValueAtRisk, ConnorsRsi, Coppock, CyberneticCycle, Decycler, DecyclerOscillator, Dema, DerivativeOscillator, DetrendedStdDev, DisparityIndex, DoubleBollinger, Dpo, DrawdownDuration, DynamicMomentumIndex, EhlersStochastic, Ehma, ElderImpulse, Ema, EmpiricalModeDecomposition, EwmaVolatility, Expectancy, Fama, FisherRsi, FisherTransform, Frama, GainLossRatio, Garch11, 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, MedianChannel, MedianMa, MidPoint, Mom, OmegaRatio, PainIndex, PearsonCorrelation, PercentageTrailingStop, Pmo, PolarizedFractalEfficiency, Ppo, PpoHistogram, ProfitFactor, Qqe, QuartileBands, 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, VolatilityOfVolatility, 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.
|
||||
@@ -272,6 +272,27 @@ fuzz_target!(|data: Vec<f64>| {
|
||||
}
|
||||
|
||||
// --- Family 05: scalar-input band/channel indicators (multi-output) ---
|
||||
{
|
||||
let mut medianchannel = MedianChannel::new(5, 2.0).unwrap();
|
||||
for &x in &data {
|
||||
let _ = medianchannel.update(x);
|
||||
}
|
||||
let _ = MedianChannel::new(5, 2.0).unwrap().batch(&data);
|
||||
}
|
||||
{
|
||||
let mut bomarbands = BomarBands::new(4, 0.85).unwrap();
|
||||
for &x in &data {
|
||||
let _ = bomarbands.update(x);
|
||||
}
|
||||
let _ = BomarBands::new(4, 0.85).unwrap().batch(&data);
|
||||
}
|
||||
{
|
||||
let mut quartilebands = QuartileBands::new(4).unwrap();
|
||||
for &x in &data {
|
||||
let _ = quartilebands.update(x);
|
||||
}
|
||||
let _ = QuartileBands::new(4).unwrap().batch(&data);
|
||||
}
|
||||
{
|
||||
let mut env = MaEnvelope::new(20, 0.025).unwrap();
|
||||
for &x in &data {
|
||||
|
||||
@@ -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, 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, VolatilityCone, VolatilityRatio, 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, ProjectionBands, ProjectionOscillator, 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, VolatilityCone, VolatilityRatio, 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
|
||||
@@ -73,6 +73,8 @@ fuzz_target!(|data: Vec<f64>| {
|
||||
drive(|| YangZhangVolatility::new(20, 252).unwrap(), &candles);
|
||||
|
||||
// --- Bands & Channels ---
|
||||
drive(|| ProjectionOscillator::new(14).unwrap(), &candles);
|
||||
drive(|| ProjectionBands::new(3).unwrap(), &candles);
|
||||
drive(|| Keltner::new(20, 10, 2.0).unwrap(), &candles);
|
||||
drive(|| Donchian::new(20).unwrap(), &candles);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user