Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0d2acad28d | |||
| b228a70d7d |
+11
-1
@@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.5.5] - 2026-06-04
|
||||
- **GD** — generalized DEMA (GD), Tillson's volume-factor double EMA and the building block of T3 (`GD`).
|
||||
- **GMA** — geometric moving average (GMA), the rolling geometric mean of prices (`GMA`).
|
||||
- **Holt-Winters** — Holt's linear (double exponential) smoothing with level and trend components (`HoltWinters`).
|
||||
- **Adaptive Laguerre** — Ehlers adaptive Laguerre filter with median-error-adaptive gamma (`AdaptiveLaguerre`).
|
||||
- **Median MA** — median moving average, the rolling median of prices (`MedianMA`).
|
||||
- **EHMA** — exponential Hull moving average (EHMA), the Hull construction built from EMAs (`EHMA`).
|
||||
- **SWMA** — sine-weighted moving average (SWMA), a symmetric half-cycle sine window (`SWMA`).
|
||||
|
||||
## [0.5.4] - 2026-06-04
|
||||
- **Roll Measure** — effective spread implied by the negative serial covariance of trade-price changes (Roll 1984) (`RollMeasure`).
|
||||
- **Amihud Illiquidity** — average absolute log return per unit of traded value (price-impact liquidity proxy, Amihud 2002) (`AmihudIlliquidity`).
|
||||
@@ -1238,7 +1247,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.5.4...HEAD
|
||||
[Unreleased]: https://github.com/wickra-lib/wickra/compare/v0.5.5...HEAD
|
||||
[0.5.5]: https://github.com/wickra-lib/wickra/compare/v0.5.4...v0.5.5
|
||||
[0.5.4]: https://github.com/wickra-lib/wickra/compare/v0.5.3...v0.5.4
|
||||
[0.5.3]: https://github.com/wickra-lib/wickra/compare/v0.5.2...v0.5.3
|
||||
[0.5.2]: https://github.com/wickra-lib/wickra/compare/v0.5.1...v0.5.2
|
||||
|
||||
Generated
+6
-6
@@ -1867,7 +1867,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra"
|
||||
version = "0.5.4"
|
||||
version = "0.5.5"
|
||||
dependencies = [
|
||||
"approx",
|
||||
"criterion",
|
||||
@@ -1878,7 +1878,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-core"
|
||||
version = "0.5.4"
|
||||
version = "0.5.5"
|
||||
dependencies = [
|
||||
"approx",
|
||||
"proptest",
|
||||
@@ -1888,7 +1888,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-data"
|
||||
version = "0.5.4"
|
||||
version = "0.5.5"
|
||||
dependencies = [
|
||||
"approx",
|
||||
"csv",
|
||||
@@ -1915,7 +1915,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-node"
|
||||
version = "0.5.4"
|
||||
version = "0.5.5"
|
||||
dependencies = [
|
||||
"napi",
|
||||
"napi-build",
|
||||
@@ -1925,7 +1925,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-python"
|
||||
version = "0.5.4"
|
||||
version = "0.5.5"
|
||||
dependencies = [
|
||||
"numpy",
|
||||
"pyo3",
|
||||
@@ -1934,7 +1934,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-wasm"
|
||||
version = "0.5.4"
|
||||
version = "0.5.5"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"js-sys",
|
||||
|
||||
+2
-2
@@ -12,7 +12,7 @@ members = [
|
||||
exclude = ["fuzz"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.5.4"
|
||||
version = "0.5.5"
|
||||
authors = ["kingchenc <support@wickra.org>"]
|
||||
edition = "2021"
|
||||
rust-version = "1.86"
|
||||
@@ -24,7 +24,7 @@ keywords = ["finance", "trading", "indicators", "technical-analysis", "ta"]
|
||||
categories = ["finance", "mathematics", "science"]
|
||||
|
||||
[workspace.dependencies]
|
||||
wickra-core = { path = "crates/wickra-core", version = "0.5.4" }
|
||||
wickra-core = { path = "crates/wickra-core", version = "0.5.5" }
|
||||
|
||||
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=396" 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=403" 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 396 indicators; start at the
|
||||
every one of the 403 indicators; start at the
|
||||
[indicators overview](https://docs.wickra.org/Indicators-Overview).
|
||||
- **Reference** — [warmup periods](https://docs.wickra.org/Warmup-Periods),
|
||||
[streaming vs batch](https://docs.wickra.org/Streaming-vs-Batch),
|
||||
@@ -136,14 +136,14 @@ python -m benchmarks.compare_libraries
|
||||
|
||||
## Indicators
|
||||
|
||||
396 streaming-first indicators across twenty-four families. Every one passes the
|
||||
403 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).
|
||||
|
||||
| Family | Indicators |
|
||||
|--------|-----------|
|
||||
| Moving Averages | SMA, EMA, WMA, DEMA, TEMA, HMA, KAMA, SMMA, TRIMA, ZLEMA, T3, VWMA, ALMA, McGinley Dynamic, FRAMA, VIDYA, JMA, Alligator, EVWMA |
|
||||
| Moving Averages | SMA, EMA, WMA, DEMA, TEMA, HMA, KAMA, SMMA, TRIMA, ZLEMA, T3, VWMA, ALMA, McGinley Dynamic, FRAMA, VIDYA, JMA, Alligator, EVWMA, SWMA, GMA, EHMA, Median MA, Adaptive Laguerre, GD, Holt-Winters |
|
||||
| Momentum Oscillators | RSI (Wilder), Anchored RSI, Stochastic, CCI, ROC, Williams %R, MFI, Awesome Oscillator, MOM, CMO, TSI, PMO, StochRSI, Ultimate Oscillator, RVI, PGO, KST, SMI, Laguerre RSI, Connors RSI, Inertia, ROC Percentage (ROCP), ROC Ratio (ROCR), ROC Ratio 100 (ROCR100) |
|
||||
| Trend & Directional | MACD, MACD Fixed (MACDFIX), MACD Extended (MACDEXT), ADX (+DI/-DI), ADXR, Aroon, TRIX, Aroon Oscillator, Vortex, Random Walk Index, Trend Intensity Index, Wave Trend Oscillator, Mass Index, Choppiness Index, Vertical Horizontal Filter, Plus DM, Minus DM, Plus DI, Minus DI, DX |
|
||||
| Price Oscillators | PPO, DPO, Coppock, Accelerator Oscillator, Balance of Power, APO, AO Histogram, CFO, Zero-Lag MACD, Elder Impulse, STC |
|
||||
@@ -245,7 +245,7 @@ A Python live-trading example using the public `websockets` package lives at
|
||||
```
|
||||
wickra/
|
||||
├── crates/
|
||||
│ ├── wickra-core/ core engine + all 396 indicators
|
||||
│ ├── wickra-core/ core engine + all 403 indicators
|
||||
│ ├── wickra/ top-level facade crate (publishes on crates.io) + benches/
|
||||
│ └── wickra-data/ CSV reader, tick aggregator, live exchange feeds
|
||||
├── bindings/
|
||||
|
||||
@@ -28,6 +28,13 @@ function num(v) {
|
||||
// --- Scalar indicators: update(value) vs batch(prices) ---
|
||||
|
||||
const scalarFactories = {
|
||||
HoltWinters: () => new wickra.HoltWinters(0.2, 0.1),
|
||||
GD: () => new wickra.GD(5, 0.7),
|
||||
AdaptiveLaguerre: () => new wickra.AdaptiveLaguerre(13),
|
||||
MedianMA: () => new wickra.MedianMA(14),
|
||||
EHMA: () => new wickra.EHMA(9),
|
||||
GMA: () => new wickra.GMA(14),
|
||||
SWMA: () => new wickra.SWMA(14),
|
||||
Expectancy: () => new wickra.Expectancy(20),
|
||||
WinRate: () => new wickra.WinRate(20),
|
||||
RegimeLabel: () => new wickra.RegimeLabel(5, 20),
|
||||
|
||||
Vendored
+63
@@ -872,6 +872,51 @@ export declare class Expectancy {
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type SineWeightedMaNode = SWMA
|
||||
export declare class SWMA {
|
||||
constructor(period: number)
|
||||
update(value: number): number | null
|
||||
batch(prices: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type GeometricMaNode = GMA
|
||||
export declare class GMA {
|
||||
constructor(period: number)
|
||||
update(value: number): number | null
|
||||
batch(prices: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type EhmaNode = EHMA
|
||||
export declare class EHMA {
|
||||
constructor(period: number)
|
||||
update(value: number): number | null
|
||||
batch(prices: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type MedianMaNode = MedianMA
|
||||
export declare class MedianMA {
|
||||
constructor(period: number)
|
||||
update(value: number): number | null
|
||||
batch(prices: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type AdaptiveLaguerreFilterNode = AdaptiveLaguerre
|
||||
export declare class AdaptiveLaguerre {
|
||||
constructor(period: number)
|
||||
update(value: number): number | null
|
||||
batch(prices: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type JumpIndicatorNode = JumpIndicator
|
||||
export declare class JumpIndicator {
|
||||
constructor(period: number, threshold: number)
|
||||
@@ -1677,6 +1722,24 @@ export declare class T3 {
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type GeneralizedDemaNode = GD
|
||||
export declare class GD {
|
||||
constructor(period: number, v: number)
|
||||
update(value: number): number | null
|
||||
batch(prices: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type HoltWintersNode = HoltWinters
|
||||
export declare class HoltWinters {
|
||||
constructor(alpha: number, beta: number)
|
||||
update(value: number): number | null
|
||||
batch(prices: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type TsiNode = TSI
|
||||
export declare class TSI {
|
||||
constructor(long: number, short: number)
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wickra-darwin-arm64",
|
||||
"version": "0.5.4",
|
||||
"version": "0.5.5",
|
||||
"description": "Native binding for wickra (macOS Apple Silicon). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||
"main": "wickra.darwin-arm64.node",
|
||||
"files": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wickra-darwin-x64",
|
||||
"version": "0.5.4",
|
||||
"version": "0.5.5",
|
||||
"description": "Native binding for wickra (macOS Intel). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||
"main": "wickra.darwin-x64.node",
|
||||
"files": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wickra-linux-arm64-gnu",
|
||||
"version": "0.5.4",
|
||||
"version": "0.5.5",
|
||||
"description": "Native binding for wickra (linux arm64 GNU). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||
"main": "wickra.linux-arm64-gnu.node",
|
||||
"files": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wickra-linux-x64-gnu",
|
||||
"version": "0.5.4",
|
||||
"version": "0.5.5",
|
||||
"description": "Native binding for wickra (linux x64 GNU). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||
"main": "wickra.linux-x64-gnu.node",
|
||||
"files": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wickra-win32-arm64-msvc",
|
||||
"version": "0.5.4",
|
||||
"version": "0.5.5",
|
||||
"description": "Native binding for wickra (Windows arm64 MSVC). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||
"main": "wickra.win32-arm64-msvc.node",
|
||||
"files": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wickra-win32-x64-msvc",
|
||||
"version": "0.5.4",
|
||||
"version": "0.5.5",
|
||||
"description": "Native binding for wickra (Windows x64 MSVC). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||
"main": "wickra.win32-x64-msvc.node",
|
||||
"files": [
|
||||
|
||||
Generated
+20
-20
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "wickra",
|
||||
"version": "0.5.4",
|
||||
"version": "0.5.5",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "wickra",
|
||||
"version": "0.5.4",
|
||||
"version": "0.5.5",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"devDependencies": {
|
||||
"@napi-rs/cli": "^2.18.0"
|
||||
@@ -15,12 +15,12 @@
|
||||
"node": ">= 18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"wickra-darwin-arm64": "0.5.4",
|
||||
"wickra-darwin-x64": "0.5.4",
|
||||
"wickra-linux-arm64-gnu": "0.5.4",
|
||||
"wickra-linux-x64-gnu": "0.5.4",
|
||||
"wickra-win32-arm64-msvc": "0.5.4",
|
||||
"wickra-win32-x64-msvc": "0.5.4"
|
||||
"wickra-darwin-arm64": "0.5.5",
|
||||
"wickra-darwin-x64": "0.5.5",
|
||||
"wickra-linux-arm64-gnu": "0.5.5",
|
||||
"wickra-linux-x64-gnu": "0.5.5",
|
||||
"wickra-win32-arm64-msvc": "0.5.5",
|
||||
"wickra-win32-x64-msvc": "0.5.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/cli": {
|
||||
@@ -41,8 +41,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wickra-darwin-arm64": {
|
||||
"version": "0.5.4",
|
||||
"resolved": "https://registry.npmjs.org/wickra-darwin-arm64/-/wickra-darwin-arm64-0.5.4.tgz",
|
||||
"version": "0.5.5",
|
||||
"resolved": "https://registry.npmjs.org/wickra-darwin-arm64/-/wickra-darwin-arm64-0.5.5.tgz",
|
||||
"integrity": "sha512-4eZiBR/yGUdr4nzhEUFy2i69XgNx64iI2ax/LPamsThgylC0KpHOZKK19QzJ2d9KbK4C8nMjME5FLuR+4GNEwQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
@@ -57,8 +57,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wickra-darwin-x64": {
|
||||
"version": "0.5.4",
|
||||
"resolved": "https://registry.npmjs.org/wickra-darwin-x64/-/wickra-darwin-x64-0.5.4.tgz",
|
||||
"version": "0.5.5",
|
||||
"resolved": "https://registry.npmjs.org/wickra-darwin-x64/-/wickra-darwin-x64-0.5.5.tgz",
|
||||
"integrity": "sha512-6hf8zI3QPjTFp4zCpmgUwDvNtu6jHqNUHKD5e55POo0CgA52HkpyxSPtVm8TGTIZDI7kPjlbOdBM8CJ76mmXwA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
@@ -73,8 +73,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wickra-linux-arm64-gnu": {
|
||||
"version": "0.5.4",
|
||||
"resolved": "https://registry.npmjs.org/wickra-linux-arm64-gnu/-/wickra-linux-arm64-gnu-0.5.4.tgz",
|
||||
"version": "0.5.5",
|
||||
"resolved": "https://registry.npmjs.org/wickra-linux-arm64-gnu/-/wickra-linux-arm64-gnu-0.5.5.tgz",
|
||||
"integrity": "sha512-kSe6y0xBMSiqdPLXNjwop5WZdHtvdBNKSEBCwZ4hFq33p4apW25/wrlzv9/oDuyD4kuPabJEhCCnFOplh58CUg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
@@ -89,8 +89,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wickra-linux-x64-gnu": {
|
||||
"version": "0.5.4",
|
||||
"resolved": "https://registry.npmjs.org/wickra-linux-x64-gnu/-/wickra-linux-x64-gnu-0.5.4.tgz",
|
||||
"version": "0.5.5",
|
||||
"resolved": "https://registry.npmjs.org/wickra-linux-x64-gnu/-/wickra-linux-x64-gnu-0.5.5.tgz",
|
||||
"integrity": "sha512-tWBWS4qz7hxM4xnpFb59bhf6TaLwXq0Z3jEa/2l7r8PiHA94g8r8S53NRMiT+4yiL5hSWe/nUiC/YXdRrhEZ4g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
@@ -105,8 +105,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wickra-win32-arm64-msvc": {
|
||||
"version": "0.5.4",
|
||||
"resolved": "https://registry.npmjs.org/wickra-win32-arm64-msvc/-/wickra-win32-arm64-msvc-0.5.4.tgz",
|
||||
"version": "0.5.5",
|
||||
"resolved": "https://registry.npmjs.org/wickra-win32-arm64-msvc/-/wickra-win32-arm64-msvc-0.5.5.tgz",
|
||||
"integrity": "sha512-EXIckHxAtF75PUGDKRzXyqMe9ldP0JjSdu68WFN6iJfp+McYrGu6h40TEJlQ/oUEIoPqiZB/xhVyo/el5Lg7zw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
@@ -121,8 +121,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wickra-win32-x64-msvc": {
|
||||
"version": "0.5.4",
|
||||
"resolved": "https://registry.npmjs.org/wickra-win32-x64-msvc/-/wickra-win32-x64-msvc-0.5.4.tgz",
|
||||
"version": "0.5.5",
|
||||
"resolved": "https://registry.npmjs.org/wickra-win32-x64-msvc/-/wickra-win32-x64-msvc-0.5.5.tgz",
|
||||
"integrity": "sha512-Yfsqq1Xwp6hdxMyLze411vNdo7BDwI6+lPSe7A9XdqyPecNDbtKwYLpsal2r8EHbNzqM+R8XnuRtUaEQS5VlUQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wickra",
|
||||
"version": "0.5.4",
|
||||
"version": "0.5.5",
|
||||
"description": "Streaming-first technical indicators: incremental, fast, install-free. Node bindings powered by Rust.",
|
||||
"author": "kingchenc <support@wickra.org>",
|
||||
"main": "index.js",
|
||||
@@ -47,12 +47,12 @@
|
||||
"node": ">= 18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"wickra-linux-x64-gnu": "0.5.4",
|
||||
"wickra-linux-arm64-gnu": "0.5.4",
|
||||
"wickra-darwin-x64": "0.5.4",
|
||||
"wickra-darwin-arm64": "0.5.4",
|
||||
"wickra-win32-x64-msvc": "0.5.4",
|
||||
"wickra-win32-arm64-msvc": "0.5.4"
|
||||
"wickra-linux-x64-gnu": "0.5.5",
|
||||
"wickra-linux-arm64-gnu": "0.5.5",
|
||||
"wickra-darwin-x64": "0.5.5",
|
||||
"wickra-darwin-arm64": "0.5.5",
|
||||
"wickra-win32-x64-msvc": "0.5.5",
|
||||
"wickra-win32-arm64-msvc": "0.5.5"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "napi build --platform --release",
|
||||
|
||||
@@ -197,6 +197,15 @@ node_scalar_indicator!(
|
||||
node_scalar_indicator!(TrendLabelNode, "TrendLabel", wc::TrendLabel);
|
||||
node_scalar_indicator!(WinRateNode, "WinRate", wc::WinRate);
|
||||
node_scalar_indicator!(ExpectancyNode, "Expectancy", wc::Expectancy);
|
||||
node_scalar_indicator!(SineWeightedMaNode, "SWMA", wc::SineWeightedMa);
|
||||
node_scalar_indicator!(GeometricMaNode, "GMA", wc::GeometricMa);
|
||||
node_scalar_indicator!(EhmaNode, "EHMA", wc::Ehma);
|
||||
node_scalar_indicator!(MedianMaNode, "MedianMA", wc::MedianMa);
|
||||
node_scalar_indicator!(
|
||||
AdaptiveLaguerreFilterNode,
|
||||
"AdaptiveLaguerre",
|
||||
wc::AdaptiveLaguerreFilter
|
||||
);
|
||||
#[napi(js_name = "JumpIndicator")]
|
||||
pub struct JumpIndicatorNode {
|
||||
inner: wc::JumpIndicator,
|
||||
@@ -3794,6 +3803,80 @@ impl T3Node {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== GD ==============================
|
||||
|
||||
#[napi(js_name = "GD")]
|
||||
pub struct GeneralizedDemaNode {
|
||||
inner: wc::GeneralizedDema,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl GeneralizedDemaNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(period: u32, v: f64) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::GeneralizedDema::new(period as usize, v).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(&mut self, value: f64) -> Option<f64> {
|
||||
self.inner.update(value)
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(&mut self, prices: Vec<f64>) -> Vec<f64> {
|
||||
flatten(self.inner.batch(&prices))
|
||||
}
|
||||
#[napi]
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
#[napi(js_name = "isReady")]
|
||||
pub fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
#[napi(js_name = "warmupPeriod")]
|
||||
pub fn warmup_period(&self) -> u32 {
|
||||
self.inner.warmup_period() as u32
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== HoltWinters ==============================
|
||||
|
||||
#[napi(js_name = "HoltWinters")]
|
||||
pub struct HoltWintersNode {
|
||||
inner: wc::HoltWinters,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl HoltWintersNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(alpha: f64, beta: f64) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::HoltWinters::new(alpha, beta).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(&mut self, value: f64) -> Option<f64> {
|
||||
self.inner.update(value)
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(&mut self, prices: Vec<f64>) -> Vec<f64> {
|
||||
flatten(self.inner.batch(&prices))
|
||||
}
|
||||
#[napi]
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
#[napi(js_name = "isReady")]
|
||||
pub fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
#[napi(js_name = "warmupPeriod")]
|
||||
pub fn warmup_period(&self) -> u32 {
|
||||
self.inner.warmup_period() as u32
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== TSI ==============================
|
||||
|
||||
#[napi(js_name = "TSI")]
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "maturin"
|
||||
|
||||
[project]
|
||||
name = "wickra"
|
||||
version = "0.5.4"
|
||||
version = "0.5.5"
|
||||
description = "Streaming-first technical indicators: incremental, fast, install-free."
|
||||
readme = "README.md"
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
@@ -25,6 +25,13 @@ from __future__ import annotations
|
||||
|
||||
from ._wickra import (
|
||||
__version__,
|
||||
HoltWinters,
|
||||
GD,
|
||||
AdaptiveLaguerre,
|
||||
MedianMA,
|
||||
EHMA,
|
||||
GMA,
|
||||
SWMA,
|
||||
Expectancy,
|
||||
WinRate,
|
||||
RegimeLabel,
|
||||
@@ -449,6 +456,13 @@ from ._wickra import (
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"HoltWinters",
|
||||
"GD",
|
||||
"AdaptiveLaguerre",
|
||||
"MedianMA",
|
||||
"EHMA",
|
||||
"GMA",
|
||||
"SWMA",
|
||||
"Expectancy",
|
||||
"WinRate",
|
||||
"RegimeLabel",
|
||||
|
||||
@@ -2354,6 +2354,250 @@ impl PyExpectancy {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== SineWeightedMa ==============================
|
||||
|
||||
#[pyclass(name = "SWMA", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PySineWeightedMa {
|
||||
inner: wc::SineWeightedMa,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PySineWeightedMa {
|
||||
#[new]
|
||||
#[pyo3(signature = (period=14))]
|
||||
fn new(period: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::SineWeightedMa::new(period).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
fn update(&mut self, value: f64) -> Option<f64> {
|
||||
self.inner.update(value)
|
||||
}
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
prices: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
let s = prices
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
Ok(flatten(self.inner.batch(s)).into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn period(&self) -> usize {
|
||||
self.inner.period()
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.inner.warmup_period()
|
||||
}
|
||||
fn __repr__(&self) -> String {
|
||||
format!("SWMA(period={})", self.inner.period())
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== GeometricMa ==============================
|
||||
|
||||
#[pyclass(name = "GMA", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyGeometricMa {
|
||||
inner: wc::GeometricMa,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyGeometricMa {
|
||||
#[new]
|
||||
#[pyo3(signature = (period=14))]
|
||||
fn new(period: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::GeometricMa::new(period).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
fn update(&mut self, value: f64) -> Option<f64> {
|
||||
self.inner.update(value)
|
||||
}
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
prices: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
let s = prices
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
Ok(flatten(self.inner.batch(s)).into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn period(&self) -> usize {
|
||||
self.inner.period()
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.inner.warmup_period()
|
||||
}
|
||||
fn __repr__(&self) -> String {
|
||||
format!("GMA(period={})", self.inner.period())
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Ehma ==============================
|
||||
|
||||
#[pyclass(name = "EHMA", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyEhma {
|
||||
inner: wc::Ehma,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyEhma {
|
||||
#[new]
|
||||
#[pyo3(signature = (period=9))]
|
||||
fn new(period: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::Ehma::new(period).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
fn update(&mut self, value: f64) -> Option<f64> {
|
||||
self.inner.update(value)
|
||||
}
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
prices: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
let s = prices
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
Ok(flatten(self.inner.batch(s)).into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn period(&self) -> usize {
|
||||
self.inner.period()
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.inner.warmup_period()
|
||||
}
|
||||
fn __repr__(&self) -> String {
|
||||
format!("EHMA(period={})", self.inner.period())
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== MedianMa ==============================
|
||||
|
||||
#[pyclass(name = "MedianMA", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyMedianMa {
|
||||
inner: wc::MedianMa,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyMedianMa {
|
||||
#[new]
|
||||
#[pyo3(signature = (period=14))]
|
||||
fn new(period: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::MedianMa::new(period).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
fn update(&mut self, value: f64) -> Option<f64> {
|
||||
self.inner.update(value)
|
||||
}
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
prices: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
let s = prices
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
Ok(flatten(self.inner.batch(s)).into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn period(&self) -> usize {
|
||||
self.inner.period()
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.inner.warmup_period()
|
||||
}
|
||||
fn __repr__(&self) -> String {
|
||||
format!("MedianMA(period={})", self.inner.period())
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== AdaptiveLaguerreFilter ==============================
|
||||
|
||||
#[pyclass(
|
||||
name = "AdaptiveLaguerre",
|
||||
module = "wickra._wickra",
|
||||
skip_from_py_object
|
||||
)]
|
||||
#[derive(Clone)]
|
||||
struct PyAdaptiveLaguerreFilter {
|
||||
inner: wc::AdaptiveLaguerreFilter,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyAdaptiveLaguerreFilter {
|
||||
#[new]
|
||||
#[pyo3(signature = (period=13))]
|
||||
fn new(period: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::AdaptiveLaguerreFilter::new(period).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
fn update(&mut self, value: f64) -> Option<f64> {
|
||||
self.inner.update(value)
|
||||
}
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
prices: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
let s = prices
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
Ok(flatten(self.inner.batch(s)).into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn period(&self) -> usize {
|
||||
self.inner.period()
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.inner.warmup_period()
|
||||
}
|
||||
fn __repr__(&self) -> String {
|
||||
format!("AdaptiveLaguerre(period={})", self.inner.period())
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Stochastic ==============================
|
||||
|
||||
#[pyclass(name = "Stochastic", module = "wickra._wickra", skip_from_py_object)]
|
||||
@@ -6197,6 +6441,130 @@ impl PyT3 {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== GD ==============================
|
||||
|
||||
#[pyclass(name = "GD", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyGeneralizedDema {
|
||||
inner: wc::GeneralizedDema,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyGeneralizedDema {
|
||||
#[new]
|
||||
#[pyo3(signature = (period, v=0.7))]
|
||||
fn new(period: usize, v: f64) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::GeneralizedDema::new(period, v).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
fn update(&mut self, value: f64) -> Option<f64> {
|
||||
self.inner.update(value)
|
||||
}
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
prices: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
let slice = prices
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
Ok(flatten(self.inner.batch(slice)).into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn period(&self) -> usize {
|
||||
self.inner.period()
|
||||
}
|
||||
#[getter]
|
||||
fn volume_factor(&self) -> f64 {
|
||||
self.inner.volume_factor()
|
||||
}
|
||||
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!(
|
||||
"GD(period={}, v={})",
|
||||
self.inner.period(),
|
||||
self.inner.volume_factor()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== HoltWinters ==============================
|
||||
|
||||
#[pyclass(name = "HoltWinters", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyHoltWinters {
|
||||
inner: wc::HoltWinters,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyHoltWinters {
|
||||
#[new]
|
||||
#[pyo3(signature = (alpha=0.2, beta=0.1))]
|
||||
fn new(alpha: f64, beta: f64) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::HoltWinters::new(alpha, beta).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
fn update(&mut self, value: f64) -> Option<f64> {
|
||||
self.inner.update(value)
|
||||
}
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
prices: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
let slice = prices
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
Ok(flatten(self.inner.batch(slice)).into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn alpha(&self) -> f64 {
|
||||
self.inner.alpha()
|
||||
}
|
||||
#[getter]
|
||||
fn beta(&self) -> f64 {
|
||||
self.inner.beta()
|
||||
}
|
||||
#[getter]
|
||||
fn level(&self) -> Option<f64> {
|
||||
self.inner.level()
|
||||
}
|
||||
#[getter]
|
||||
fn trend(&self) -> Option<f64> {
|
||||
self.inner.trend()
|
||||
}
|
||||
#[getter]
|
||||
fn value(&self) -> Option<f64> {
|
||||
self.inner.value()
|
||||
}
|
||||
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!(
|
||||
"HoltWinters(alpha={}, beta={})",
|
||||
self.inner.alpha(),
|
||||
self.inner.beta()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== VWMA ==============================
|
||||
|
||||
#[pyclass(name = "VWMA", module = "wickra._wickra", skip_from_py_object)]
|
||||
@@ -19667,6 +20035,8 @@ fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_class::<PyTrima>()?;
|
||||
m.add_class::<PyZlema>()?;
|
||||
m.add_class::<PyT3>()?;
|
||||
m.add_class::<PyGeneralizedDema>()?;
|
||||
m.add_class::<PyHoltWinters>()?;
|
||||
m.add_class::<PyVwma>()?;
|
||||
m.add_class::<PyMom>()?;
|
||||
m.add_class::<PyCmo>()?;
|
||||
@@ -20031,5 +20401,10 @@ fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_class::<PyRegimeLabel>()?;
|
||||
m.add_class::<PyWinRate>()?;
|
||||
m.add_class::<PyExpectancy>()?;
|
||||
m.add_class::<PySineWeightedMa>()?;
|
||||
m.add_class::<PyGeometricMa>()?;
|
||||
m.add_class::<PyEhma>()?;
|
||||
m.add_class::<PyMedianMa>()?;
|
||||
m.add_class::<PyAdaptiveLaguerreFilter>()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -45,6 +45,13 @@ def ohlcv() -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
|
||||
# --- Scalar (f64 -> f64) indicators ---------------------------------------
|
||||
|
||||
SCALAR = [
|
||||
(ta.HoltWinters, (0.2, 0.1)),
|
||||
(ta.GD, (5, 0.7)),
|
||||
(ta.AdaptiveLaguerre, (13,)),
|
||||
(ta.MedianMA, (14,)),
|
||||
(ta.EHMA, (9,)),
|
||||
(ta.GMA, (14,)),
|
||||
(ta.SWMA, (14,)),
|
||||
(ta.Expectancy, (20,)),
|
||||
(ta.WinRate, (20,)),
|
||||
(ta.RegimeLabel, (5, 20)),
|
||||
|
||||
@@ -10213,6 +10213,13 @@ wasm_scalar_indicator!(WasmJumpIndicator, "JumpIndicator", wc::JumpIndicator, pe
|
||||
wasm_scalar_indicator!(WasmRegimeLabel, "RegimeLabel", wc::RegimeLabel, vol_period: usize, lookback: usize);
|
||||
wasm_scalar_indicator!(WasmWinRate, "WinRate", wc::WinRate, period: usize);
|
||||
wasm_scalar_indicator!(WasmExpectancy, "Expectancy", wc::Expectancy, period: usize);
|
||||
wasm_scalar_indicator!(WasmSineWeightedMa, "SWMA", wc::SineWeightedMa, period: usize);
|
||||
wasm_scalar_indicator!(WasmGeometricMa, "GMA", wc::GeometricMa, period: usize);
|
||||
wasm_scalar_indicator!(WasmEhma, "EHMA", wc::Ehma, period: usize);
|
||||
wasm_scalar_indicator!(WasmMedianMa, "MedianMA", wc::MedianMa, period: usize);
|
||||
wasm_scalar_indicator!(WasmAdaptiveLaguerreFilter, "AdaptiveLaguerre", wc::AdaptiveLaguerreFilter, period: usize);
|
||||
wasm_scalar_indicator!(WasmGeneralizedDema, "GD", wc::GeneralizedDema, period: usize, v: f64);
|
||||
wasm_scalar_indicator!(WasmHoltWinters, "HoltWinters", wc::HoltWinters, alpha: f64, beta: f64);
|
||||
|
||||
// --- DrawdownDuration: u32 output, no constructor args ---
|
||||
|
||||
|
||||
@@ -0,0 +1,344 @@
|
||||
//! Ehlers' Adaptive Laguerre Filter.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// John Ehlers' Adaptive Laguerre Filter — a four-stage Laguerre polynomial
|
||||
/// smoother whose damping factor `gamma` is recomputed every bar from how well
|
||||
/// the filter is currently tracking price.
|
||||
///
|
||||
/// The Laguerre cascade is the same one used by [`LaguerreRsi`](crate::LaguerreRsi),
|
||||
/// but instead of a fixed `gamma` the filter adapts: it measures the recent
|
||||
/// absolute error `|price − filter|`, normalises those errors across a window of
|
||||
/// `period` bars to `[0, 1]`, and takes their **median** as `gamma`. When price
|
||||
/// is tracking smoothly the errors are small and uniform (low `gamma`, fast
|
||||
/// response); when price jumps, the spread of errors widens and `gamma` rises,
|
||||
/// slowing the filter to reject the noise.
|
||||
///
|
||||
/// ```text
|
||||
/// diff_t = |price_t − filter_{t-1}|
|
||||
/// over the last `period` diffs:
|
||||
/// HH = max(diff), LL = min(diff)
|
||||
/// norm_i = (diff_i − LL) / (HH − LL) (0 if HH == LL)
|
||||
/// gamma = median(norm)
|
||||
/// alpha = 1 − gamma
|
||||
/// L0_t = alpha·price_t + gamma·L0_{t-1}
|
||||
/// L1_t = −gamma·L0_t + L0_{t-1} + gamma·L1_{t-1}
|
||||
/// L2_t = −gamma·L1_t + L1_{t-1} + gamma·L2_{t-1}
|
||||
/// L3_t = −gamma·L2_t + L2_{t-1} + gamma·L3_{t-1}
|
||||
/// filter_t = (L0_t + 2·L1_t + 2·L2_t + L3_t) / 6
|
||||
/// ```
|
||||
///
|
||||
/// The output is a smoothed price on the same scale as the input. The first
|
||||
/// emission lands once the error window holds `period` values.
|
||||
///
|
||||
/// Reference: John F. Ehlers, *"Adaptive Laguerre Filter"*, Technical Analysis
|
||||
/// of Stocks & Commodities, 2007.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Indicator, AdaptiveLaguerreFilter};
|
||||
///
|
||||
/// let mut indicator = AdaptiveLaguerreFilter::new(13).unwrap();
|
||||
/// let mut last = None;
|
||||
/// for i in 0..80 {
|
||||
/// last = indicator.update(100.0 + f64::from(i));
|
||||
/// }
|
||||
/// assert!(last.is_some());
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AdaptiveLaguerreFilter {
|
||||
period: usize,
|
||||
l0: f64,
|
||||
l1: f64,
|
||||
l2: f64,
|
||||
l3: f64,
|
||||
/// Previous filter output, or `None` before the first bar.
|
||||
filter: Option<f64>,
|
||||
/// The last `period` absolute errors `|price − filter|`.
|
||||
diffs: VecDeque<f64>,
|
||||
}
|
||||
|
||||
impl AdaptiveLaguerreFilter {
|
||||
/// Construct a new adaptive Laguerre filter with the given error-window
|
||||
/// length.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::PeriodZero`] if `period == 0`.
|
||||
pub fn new(period: usize) -> Result<Self> {
|
||||
if period == 0 {
|
||||
return Err(Error::PeriodZero);
|
||||
}
|
||||
Ok(Self {
|
||||
period,
|
||||
l0: 0.0,
|
||||
l1: 0.0,
|
||||
l2: 0.0,
|
||||
l3: 0.0,
|
||||
filter: None,
|
||||
diffs: VecDeque::with_capacity(period),
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured error-window length.
|
||||
pub const fn period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
|
||||
/// Current value if the error window is full.
|
||||
pub fn value(&self) -> Option<f64> {
|
||||
if self.diffs.len() == self.period {
|
||||
self.filter
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Median of the normalised errors currently in the window. Returns `0.0`
|
||||
/// when every error is equal (e.g. during a constant warmup), which makes
|
||||
/// the filter maximally fast.
|
||||
fn adaptive_gamma(&self) -> f64 {
|
||||
let mut hh = f64::MIN;
|
||||
let mut ll = f64::MAX;
|
||||
for &d in &self.diffs {
|
||||
if d > hh {
|
||||
hh = d;
|
||||
}
|
||||
if d < ll {
|
||||
ll = d;
|
||||
}
|
||||
}
|
||||
let range = hh - ll;
|
||||
if range <= 0.0 {
|
||||
return 0.0;
|
||||
}
|
||||
let mut norm: Vec<f64> = self.diffs.iter().map(|&d| (d - ll) / range).collect();
|
||||
// `total_cmp` never panics — under pathological (e.g. overflowing) fuzz
|
||||
// inputs a normalised error can be non-finite; a total order keeps the
|
||||
// sort sound where `partial_cmp` would return `None`.
|
||||
norm.sort_by(f64::total_cmp);
|
||||
let mid = norm.len() / 2;
|
||||
if norm.len() % 2 == 1 {
|
||||
norm[mid]
|
||||
} else {
|
||||
f64::midpoint(norm[mid - 1], norm[mid])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for AdaptiveLaguerreFilter {
|
||||
type Input = f64;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, price: f64) -> Option<f64> {
|
||||
if !price.is_finite() {
|
||||
return self.value();
|
||||
}
|
||||
// Absolute tracking error against the previous filter (0 on the first
|
||||
// bar, where there is no prior filter value).
|
||||
let diff = self.filter.map_or(0.0, |f| (price - f).abs());
|
||||
if self.diffs.len() == self.period {
|
||||
self.diffs.pop_front();
|
||||
}
|
||||
self.diffs.push_back(diff);
|
||||
|
||||
let gamma = self.adaptive_gamma();
|
||||
let alpha = 1.0 - gamma;
|
||||
|
||||
let l0 = alpha * price + gamma * self.l0;
|
||||
let l1 = -gamma * l0 + self.l0 + gamma * self.l1;
|
||||
let l2 = -gamma * l1 + self.l1 + gamma * self.l2;
|
||||
let l3 = -gamma * l2 + self.l2 + gamma * self.l3;
|
||||
self.l0 = l0;
|
||||
self.l1 = l1;
|
||||
self.l2 = l2;
|
||||
self.l3 = l3;
|
||||
|
||||
let filter = (l0 + 2.0 * l1 + 2.0 * l2 + l3) / 6.0;
|
||||
self.filter = Some(filter);
|
||||
self.value()
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.l0 = 0.0;
|
||||
self.l1 = 0.0;
|
||||
self.l2 = 0.0;
|
||||
self.l3 = 0.0;
|
||||
self.filter = None;
|
||||
self.diffs.clear();
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.diffs.len() == self.period
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"AdaptiveLaguerre"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
/// Independent reference: replays the exact recurrence from scratch.
|
||||
fn naive(prices: &[f64], period: usize) -> Vec<Option<f64>> {
|
||||
let (mut l0, mut l1, mut l2, mut l3) = (0.0_f64, 0.0_f64, 0.0_f64, 0.0_f64);
|
||||
let mut filter: Option<f64> = None;
|
||||
let mut diffs: Vec<f64> = Vec::new();
|
||||
let mut out = Vec::with_capacity(prices.len());
|
||||
for &price in prices {
|
||||
let diff = filter.map_or(0.0, |f: f64| (price - f).abs());
|
||||
diffs.push(diff);
|
||||
if diffs.len() > period {
|
||||
diffs.remove(0);
|
||||
}
|
||||
let hh = diffs.iter().copied().fold(f64::MIN, f64::max);
|
||||
let ll = diffs.iter().copied().fold(f64::MAX, f64::min);
|
||||
let range = hh - ll;
|
||||
let gamma = if range <= 0.0 {
|
||||
0.0
|
||||
} else {
|
||||
let mut norm: Vec<f64> = diffs.iter().map(|&d| (d - ll) / range).collect();
|
||||
norm.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||
let mid = norm.len() / 2;
|
||||
if norm.len() % 2 == 1 {
|
||||
norm[mid]
|
||||
} else {
|
||||
f64::midpoint(norm[mid - 1], norm[mid])
|
||||
}
|
||||
};
|
||||
let alpha = 1.0 - gamma;
|
||||
let n0 = alpha * price + gamma * l0;
|
||||
let n1 = -gamma * n0 + l0 + gamma * l1;
|
||||
let n2 = -gamma * n1 + l1 + gamma * l2;
|
||||
let n3 = -gamma * n2 + l2 + gamma * l3;
|
||||
l0 = n0;
|
||||
l1 = n1;
|
||||
l2 = n2;
|
||||
l3 = n3;
|
||||
let f = (n0 + 2.0 * n1 + 2.0 * n2 + n3) / 6.0;
|
||||
filter = Some(f);
|
||||
out.push(if diffs.len() == period { Some(f) } else { None });
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_rejects_zero_period() {
|
||||
assert!(matches!(
|
||||
AdaptiveLaguerreFilter::new(0),
|
||||
Err(Error::PeriodZero)
|
||||
));
|
||||
}
|
||||
|
||||
/// Cover the const accessor `period` and the Indicator-impl `warmup_period`
|
||||
/// + `name`.
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let alf = AdaptiveLaguerreFilter::new(13).unwrap();
|
||||
assert_eq!(alf.period(), 13);
|
||||
assert_eq!(alf.warmup_period(), 13);
|
||||
assert_eq!(alf.name(), "AdaptiveLaguerre");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn warmup_returns_none_until_window_full() {
|
||||
let mut alf = AdaptiveLaguerreFilter::new(3).unwrap();
|
||||
assert_eq!(alf.update(10.0), None);
|
||||
assert_eq!(alf.update(11.0), None);
|
||||
assert!(alf.update(12.0).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn constant_series_converges_to_constant() {
|
||||
// Errors are all zero -> gamma 0 -> the 4-stage delay line fills with
|
||||
// the constant and the filter settles on it.
|
||||
let mut alf = AdaptiveLaguerreFilter::new(5).unwrap();
|
||||
let out = alf.batch(&[42.0_f64; 40]);
|
||||
let last = out.iter().rev().flatten().next().unwrap();
|
||||
assert_relative_eq!(*last, 42.0, epsilon = 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converged_output_stays_within_price_range() {
|
||||
// Once the Laguerre cascade has filled (it cold-starts from zero, so the
|
||||
// first few post-warmup values ramp up toward price), the filter is a
|
||||
// convex blend of recent prices and must stay inside the data range.
|
||||
let prices: Vec<f64> = (0..120)
|
||||
.map(|i| 50.0 + (f64::from(i) * 0.4).sin() * 10.0)
|
||||
.collect();
|
||||
let lo = prices.iter().copied().fold(f64::MAX, f64::min);
|
||||
let hi = prices.iter().copied().fold(f64::MIN, f64::max);
|
||||
let period = 8;
|
||||
let mut alf = AdaptiveLaguerreFilter::new(period).unwrap();
|
||||
for (i, v) in alf.batch(&prices).into_iter().enumerate() {
|
||||
// Skip the cold-start transient (a few multiples of the window).
|
||||
if i < 4 * period {
|
||||
continue;
|
||||
}
|
||||
let v = v.expect("filter is ready well past warmup");
|
||||
assert!(
|
||||
v >= lo - 1e-6 && v <= hi + 1e-6,
|
||||
"filter out of range at {i}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn matches_naive_recurrence() {
|
||||
let prices: Vec<f64> = (0..80)
|
||||
.map(|i| 100.0 + (f64::from(i) * 0.5).sin() * 8.0 + f64::from(i) * 0.1)
|
||||
.collect();
|
||||
let mut alf = AdaptiveLaguerreFilter::new(10).unwrap();
|
||||
let got = alf.batch(&prices);
|
||||
let want = naive(&prices, 10);
|
||||
for (i, (g, w)) in got.iter().zip(want.iter()).enumerate() {
|
||||
assert_eq!(g.is_some(), w.is_some(), "readiness mismatch at {i}");
|
||||
if let (Some(a), Some(b)) = (g, w) {
|
||||
assert_relative_eq!(*a, *b, epsilon = 1e-9);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut alf = AdaptiveLaguerreFilter::new(5).unwrap();
|
||||
alf.batch(&(1..=40).map(f64::from).collect::<Vec<_>>());
|
||||
assert!(alf.is_ready());
|
||||
alf.reset();
|
||||
assert!(!alf.is_ready());
|
||||
assert_eq!(alf.update(1.0), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let prices: Vec<f64> = (1..=50).map(|i| f64::from(i) * 0.7).collect();
|
||||
let mut a = AdaptiveLaguerreFilter::new(7).unwrap();
|
||||
let mut b = AdaptiveLaguerreFilter::new(7).unwrap();
|
||||
assert_eq!(
|
||||
a.batch(&prices),
|
||||
prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_non_finite_input() {
|
||||
let mut alf = AdaptiveLaguerreFilter::new(3).unwrap();
|
||||
alf.update(10.0);
|
||||
alf.update(11.0);
|
||||
let ready = alf.update(12.0).expect("ready after three inputs");
|
||||
assert_eq!(alf.update(f64::NAN), Some(ready));
|
||||
assert_eq!(alf.update(f64::INFINITY), Some(ready));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
//! Exponential Hull Moving Average (EHMA).
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::indicators::ema::Ema;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Exponential Hull Moving Average: the Hull construction built from EMAs
|
||||
/// instead of WMAs.
|
||||
///
|
||||
/// ```text
|
||||
/// EHMA = EMA( 2 · EMA(price, period/2) − EMA(price, period), round(sqrt(period)) )
|
||||
/// ```
|
||||
///
|
||||
/// Alan Hull's [`Hma`](crate::Hma) uses weighted moving averages; replacing them
|
||||
/// with exponential moving averages keeps the same lag-reduction trick — a fast
|
||||
/// half-length average minus a full-length one, smoothed over `sqrt(period)` —
|
||||
/// while inheriting the EMA's strictly recursive O(1) update and infinite
|
||||
/// (exponentially decaying) memory. The result is marginally smoother than the
|
||||
/// WMA-based Hull at the cost of a little more lag.
|
||||
///
|
||||
/// The half period is `(period / 2).max(1)` and the smoothing period is
|
||||
/// `round(sqrt(period)).max(1)`, matching the rounding used by [`Hma`].
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Indicator, Ehma};
|
||||
///
|
||||
/// let mut indicator = Ehma::new(9).unwrap();
|
||||
/// let mut last = None;
|
||||
/// for i in 0..80 {
|
||||
/// last = indicator.update(100.0 + f64::from(i));
|
||||
/// }
|
||||
/// assert!(last.is_some());
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Ehma {
|
||||
period: usize,
|
||||
half_ema: Ema,
|
||||
full_ema: Ema,
|
||||
smooth_ema: Ema,
|
||||
}
|
||||
|
||||
impl Ehma {
|
||||
/// # Errors
|
||||
/// Returns [`Error::PeriodZero`] if `period == 0`.
|
||||
pub fn new(period: usize) -> Result<Self> {
|
||||
if period == 0 {
|
||||
return Err(Error::PeriodZero);
|
||||
}
|
||||
let half = (period / 2).max(1);
|
||||
let smooth = (period as f64).sqrt().round() as usize;
|
||||
let smooth = smooth.max(1);
|
||||
Ok(Self {
|
||||
period,
|
||||
half_ema: Ema::new(half)?,
|
||||
full_ema: Ema::new(period)?,
|
||||
smooth_ema: Ema::new(smooth)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured period.
|
||||
pub const fn period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for Ehma {
|
||||
type Input = f64;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, input: f64) -> Option<f64> {
|
||||
// Feed both component EMAs on every input so they warm up in parallel;
|
||||
// gating the longer one behind the shorter would delay the first
|
||||
// emission past `warmup_period()`.
|
||||
let h = self.half_ema.update(input);
|
||||
let f = self.full_ema.update(input);
|
||||
let (h, f) = (h?, f?);
|
||||
let diff = 2.0 * h - f;
|
||||
self.smooth_ema.update(diff)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.half_ema.reset();
|
||||
self.full_ema.reset();
|
||||
self.smooth_ema.reset();
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
// full_ema seeds at `period`, then smooth_ema needs another
|
||||
// (round(sqrt(period)) - 1) values to seed.
|
||||
let sm = (self.period as f64).sqrt().round() as usize;
|
||||
self.period + sm.max(1) - 1
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.smooth_ema.is_ready()
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"EHMA"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
#[test]
|
||||
fn constant_series_yields_constant_ehma() {
|
||||
let mut ehma = Ehma::new(9).unwrap();
|
||||
let out = ehma.batch(&[10.0_f64; 80]);
|
||||
let last = out.iter().rev().flatten().next().unwrap();
|
||||
assert_relative_eq!(*last, 10.0, epsilon = 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let prices: Vec<f64> = (1..=100).map(|i| f64::from(i) * 0.7).collect();
|
||||
let mut a = Ehma::new(9).unwrap();
|
||||
let mut b = Ehma::new(9).unwrap();
|
||||
assert_eq!(
|
||||
a.batch(&prices),
|
||||
prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut ehma = Ehma::new(9).unwrap();
|
||||
ehma.batch(&(1..=80).map(f64::from).collect::<Vec<_>>());
|
||||
assert!(ehma.is_ready());
|
||||
ehma.reset();
|
||||
assert!(!ehma.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_zero_period() {
|
||||
assert!(Ehma::new(0).is_err());
|
||||
}
|
||||
|
||||
/// Cover the const accessor `period` and the Indicator-impl `name`.
|
||||
/// `warmup_period` is covered by `first_emission_matches_warmup_period`.
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let ehma = Ehma::new(9).unwrap();
|
||||
assert_eq!(ehma.period(), 9);
|
||||
assert_eq!(ehma.name(), "EHMA");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_emission_matches_warmup_period() {
|
||||
let prices: Vec<f64> = (1..=40).map(f64::from).collect();
|
||||
let mut ehma = Ehma::new(9).unwrap();
|
||||
let out = ehma.batch(&prices);
|
||||
let warmup = ehma.warmup_period();
|
||||
// full EMA seeds at 9, smooth EMA round(sqrt(9))=3 needs 2 more -> 11.
|
||||
assert_eq!(warmup, 11);
|
||||
for (i, v) in out.iter().enumerate().take(warmup - 1) {
|
||||
assert!(v.is_none(), "index {i} must be None during warmup");
|
||||
}
|
||||
assert!(
|
||||
out[warmup - 1].is_some(),
|
||||
"first EHMA value must land at warmup_period - 1"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn matches_independent_emas() {
|
||||
// The two component EMAs run as independent siblings on the price
|
||||
// stream; EHMA must equal feeding three standalone EMAs and combining.
|
||||
let prices: Vec<f64> = (1..=50)
|
||||
.map(|i| (f64::from(i) * 0.3).sin() * 10.0 + 50.0)
|
||||
.collect();
|
||||
let mut ehma = Ehma::new(9).unwrap();
|
||||
let mut half = Ema::new(4).unwrap(); // (9 / 2).max(1)
|
||||
let mut full = Ema::new(9).unwrap();
|
||||
let mut smooth = Ema::new(3).unwrap(); // round(sqrt(9))
|
||||
for (i, &p) in prices.iter().enumerate() {
|
||||
let got = ehma.update(p);
|
||||
let want = match (half.update(p), full.update(p)) {
|
||||
(Some(h), Some(f)) => smooth.update(2.0 * h - f),
|
||||
_ => None,
|
||||
};
|
||||
assert_eq!(got.is_some(), want.is_some(), "readiness mismatch at {i}");
|
||||
if let (Some(a), Some(b)) = (got, want) {
|
||||
assert_relative_eq!(a, b, epsilon = 1e-9);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn period_one_collapses_to_pass_through() {
|
||||
// period 1: half=1, full=1, smooth=round(sqrt(1))=1; every EMA seeds on
|
||||
// the first input, so EHMA(1) passes the price straight through.
|
||||
let mut ehma = Ehma::new(1).unwrap();
|
||||
assert_relative_eq!(ehma.update(5.0).unwrap(), 5.0, epsilon = 1e-12);
|
||||
assert_relative_eq!(ehma.update(8.0).unwrap(), 8.0, epsilon = 1e-12);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
//! Generalized DEMA (GD) — Tim Tillson's volume-factor double EMA.
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::indicators::ema::Ema;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Generalized DEMA — the building block of Tillson's [`T3`](crate::T3),
|
||||
/// exposed on its own.
|
||||
///
|
||||
/// ```text
|
||||
/// GD = (1 + v) · EMA(price) − v · EMA(EMA(price))
|
||||
/// ```
|
||||
///
|
||||
/// where both EMAs share the same `period` and `v ∈ [0, 1]` is the *volume
|
||||
/// factor*. `v` controls how much of the second-order lag correction is
|
||||
/// applied:
|
||||
///
|
||||
/// - `v = 0` collapses GD to a plain [`Ema`](crate::Ema) (no correction).
|
||||
/// - `v = 1` recovers the standard [`Dema`](crate::Dema) `2·EMA − EMA(EMA)`.
|
||||
/// - intermediate values (Tillson uses `0.7`) trade a little lag reduction for
|
||||
/// less overshoot than DEMA.
|
||||
///
|
||||
/// Because the coefficients `(1 + v)` and `−v` always sum to `1`, a constant
|
||||
/// series maps to itself. The first output lands after `2·period − 1` inputs —
|
||||
/// EMA1 seeds at `period`, then EMA2 needs another `period − 1` of EMA1's
|
||||
/// outputs to seed, exactly like DEMA.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Indicator, GeneralizedDema};
|
||||
///
|
||||
/// let mut indicator = GeneralizedDema::new(5, 0.7).unwrap();
|
||||
/// let mut last = None;
|
||||
/// for i in 0..80 {
|
||||
/// last = indicator.update(100.0 + f64::from(i));
|
||||
/// }
|
||||
/// assert!(last.is_some());
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GeneralizedDema {
|
||||
ema1: Ema,
|
||||
ema2: Ema,
|
||||
period: usize,
|
||||
v: f64,
|
||||
}
|
||||
|
||||
impl GeneralizedDema {
|
||||
/// Construct a generalized DEMA with the given `period` and volume factor
|
||||
/// `v`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::PeriodZero`] if `period == 0`, or
|
||||
/// [`Error::InvalidPeriod`] if `v` is non-finite or outside `[0.0, 1.0]`.
|
||||
pub fn new(period: usize, v: f64) -> Result<Self> {
|
||||
if period == 0 {
|
||||
return Err(Error::PeriodZero);
|
||||
}
|
||||
if !v.is_finite() || !(0.0..=1.0).contains(&v) {
|
||||
return Err(Error::InvalidPeriod {
|
||||
message: "GD volume factor must be a finite value in [0.0, 1.0]",
|
||||
});
|
||||
}
|
||||
Ok(Self {
|
||||
ema1: Ema::new(period)?,
|
||||
ema2: Ema::new(period)?,
|
||||
period,
|
||||
v,
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured period.
|
||||
pub const fn period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
|
||||
/// Configured volume factor `v`.
|
||||
pub const fn volume_factor(&self) -> f64 {
|
||||
self.v
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for GeneralizedDema {
|
||||
type Input = f64;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, input: f64) -> Option<f64> {
|
||||
let e1 = self.ema1.update(input)?;
|
||||
let e2 = self.ema2.update(e1)?;
|
||||
Some((1.0 + self.v) * e1 - self.v * e2)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.ema1.reset();
|
||||
self.ema2.reset();
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
// EMA1 seeds at period, then EMA2 needs another (period - 1) values.
|
||||
2 * self.period - 1
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.ema2.is_ready()
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"GD"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::indicators::Dema;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
#[test]
|
||||
fn rejects_zero_period() {
|
||||
assert!(matches!(
|
||||
GeneralizedDema::new(0, 0.7),
|
||||
Err(Error::PeriodZero)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_invalid_volume_factor() {
|
||||
assert!(matches!(
|
||||
GeneralizedDema::new(5, -0.1),
|
||||
Err(Error::InvalidPeriod { .. })
|
||||
));
|
||||
assert!(matches!(
|
||||
GeneralizedDema::new(5, 1.5),
|
||||
Err(Error::InvalidPeriod { .. })
|
||||
));
|
||||
assert!(matches!(
|
||||
GeneralizedDema::new(5, f64::NAN),
|
||||
Err(Error::InvalidPeriod { .. })
|
||||
));
|
||||
assert!(GeneralizedDema::new(5, 0.0).is_ok());
|
||||
assert!(GeneralizedDema::new(5, 1.0).is_ok());
|
||||
}
|
||||
|
||||
/// Cover the const accessors `period` + `volume_factor` and the
|
||||
/// Indicator-impl `warmup_period` + `name`.
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let gd = GeneralizedDema::new(5, 0.7).unwrap();
|
||||
assert_eq!(gd.period(), 5);
|
||||
assert_relative_eq!(gd.volume_factor(), 0.7, epsilon = 1e-12);
|
||||
// EMA1 seeds at 5, EMA2 needs another 4 -> 2*period - 1 = 9.
|
||||
assert_eq!(gd.warmup_period(), 9);
|
||||
assert_eq!(gd.name(), "GD");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn constant_series_yields_constant() {
|
||||
let mut gd = GeneralizedDema::new(5, 0.7).unwrap();
|
||||
let out = gd.batch(&[100.0_f64; 60]);
|
||||
let last = out.iter().rev().flatten().next().unwrap();
|
||||
assert_relative_eq!(*last, 100.0, epsilon = 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v_one_equals_dema() {
|
||||
// GD with v = 1 is exactly the standard DEMA.
|
||||
let prices: Vec<f64> = (1..=80)
|
||||
.map(|i| (f64::from(i) * 0.3).sin() * 10.0 + 50.0)
|
||||
.collect();
|
||||
let mut gd = GeneralizedDema::new(7, 1.0).unwrap();
|
||||
let mut dema = Dema::new(7).unwrap();
|
||||
let gd_out = gd.batch(&prices);
|
||||
let dema_out = dema.batch(&prices);
|
||||
for (g, d) in gd_out.iter().zip(dema_out.iter()) {
|
||||
assert_eq!(g.is_some(), d.is_some());
|
||||
if let (Some(a), Some(b)) = (g, d) {
|
||||
assert_relative_eq!(*a, *b, epsilon = 1e-9);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v_zero_equals_ema() {
|
||||
// GD with v = 0 is a plain EMA (no second-order correction).
|
||||
let prices: Vec<f64> = (1..=60).map(|i| f64::from(i) * 0.5).collect();
|
||||
let mut gd = GeneralizedDema::new(6, 0.0).unwrap();
|
||||
let mut ema = Ema::new(6).unwrap();
|
||||
let gd_out = gd.batch(&prices);
|
||||
for (i, (g, p)) in gd_out.iter().zip(prices.iter()).enumerate() {
|
||||
// GD(v=0) feeds EMA1 into EMA2 but outputs EMA1 alone (coefficient
|
||||
// 1 on e1, 0 on e2); it is only ready once EMA2 is, so compare
|
||||
// against a standalone EMA chained the same way.
|
||||
let want = ema.update(*p).filter(|_| i + 1 >= gd.warmup_period());
|
||||
if let (Some(a), Some(b)) = (g, want) {
|
||||
assert_relative_eq!(*a, b, epsilon = 1e-9);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let prices: Vec<f64> = (1..=80).map(|i| f64::from(i) * 0.5).collect();
|
||||
let mut a = GeneralizedDema::new(7, 0.7).unwrap();
|
||||
let mut b = GeneralizedDema::new(7, 0.7).unwrap();
|
||||
assert_eq!(
|
||||
a.batch(&prices),
|
||||
prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut gd = GeneralizedDema::new(5, 0.7).unwrap();
|
||||
gd.batch(&(1..=50).map(f64::from).collect::<Vec<_>>());
|
||||
assert!(gd.is_ready());
|
||||
gd.reset();
|
||||
assert!(!gd.is_ready());
|
||||
assert_eq!(gd.update(1.0), None);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
//! Geometric Moving Average (GMA).
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Geometric Moving Average — the rolling geometric mean of the last `period`
|
||||
/// inputs.
|
||||
///
|
||||
/// ```text
|
||||
/// GMA = (Π value_i)^(1/period) = exp( (1/period) · Σ ln(value_i) )
|
||||
/// ```
|
||||
///
|
||||
/// The geometric mean is the natural average for *multiplicative* quantities
|
||||
/// such as prices and growth factors: averaging in log-space weights relative
|
||||
/// (percentage) moves symmetrically, so a `+10%` followed by a `−10%` move
|
||||
/// pulls the average below the start, exactly as compounded returns do. It is
|
||||
/// always less than or equal to the arithmetic mean of the same window.
|
||||
///
|
||||
/// Maintained incrementally in O(1): the running sum of natural logs is updated
|
||||
/// by adding the newcomer's log and subtracting the departing value's log as
|
||||
/// the window slides.
|
||||
///
|
||||
/// The geometric mean is only defined for **strictly positive** inputs. A
|
||||
/// non-finite or non-positive input is ignored (it leaves the window unchanged
|
||||
/// and returns the current value), mirroring the non-finite handling of the
|
||||
/// other moving averages.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Indicator, GeometricMa};
|
||||
///
|
||||
/// let mut indicator = GeometricMa::new(5).unwrap();
|
||||
/// let mut last = None;
|
||||
/// for i in 0..80 {
|
||||
/// last = indicator.update(100.0 + f64::from(i));
|
||||
/// }
|
||||
/// assert!(last.is_some());
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GeometricMa {
|
||||
period: usize,
|
||||
/// Natural logs of the values currently in the window (oldest at front).
|
||||
logs: VecDeque<f64>,
|
||||
sum_logs: f64,
|
||||
}
|
||||
|
||||
impl GeometricMa {
|
||||
/// Construct a new geometric moving average over `period` inputs.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::PeriodZero`] if `period == 0`.
|
||||
pub fn new(period: usize) -> Result<Self> {
|
||||
if period == 0 {
|
||||
return Err(Error::PeriodZero);
|
||||
}
|
||||
Ok(Self {
|
||||
period,
|
||||
logs: VecDeque::with_capacity(period),
|
||||
sum_logs: 0.0,
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured period.
|
||||
pub const fn period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
|
||||
/// Current value if the window is full.
|
||||
pub fn value(&self) -> Option<f64> {
|
||||
if self.logs.len() == self.period {
|
||||
Some((self.sum_logs / self.period as f64).exp())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for GeometricMa {
|
||||
type Input = f64;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, input: f64) -> Option<f64> {
|
||||
if !input.is_finite() || input <= 0.0 {
|
||||
return self.value();
|
||||
}
|
||||
if self.logs.len() == self.period {
|
||||
let oldest = self.logs.pop_front().expect("window non-empty");
|
||||
self.sum_logs -= oldest;
|
||||
}
|
||||
let ln = input.ln();
|
||||
self.logs.push_back(ln);
|
||||
self.sum_logs += ln;
|
||||
self.value()
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.logs.clear();
|
||||
self.sum_logs = 0.0;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.logs.len() == self.period
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"GMA"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
/// Reference implementation: explicit geometric mean over a window.
|
||||
fn gma_naive(prices: &[f64], period: usize) -> Vec<Option<f64>> {
|
||||
prices
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, _)| {
|
||||
if i + 1 < period {
|
||||
None
|
||||
} else {
|
||||
let window = &prices[i + 1 - period..=i];
|
||||
let product: f64 = window.iter().product();
|
||||
Some(product.powf(1.0 / period as f64))
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_rejects_zero_period() {
|
||||
assert!(matches!(GeometricMa::new(0), Err(Error::PeriodZero)));
|
||||
}
|
||||
|
||||
/// Cover the const accessor `period` and the Indicator-impl `warmup_period`
|
||||
/// + `name`.
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let gma = GeometricMa::new(7).unwrap();
|
||||
assert_eq!(gma.period(), 7);
|
||||
assert_eq!(gma.warmup_period(), 7);
|
||||
assert_eq!(gma.name(), "GMA");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn warmup_returns_none() {
|
||||
let mut gma = GeometricMa::new(3).unwrap();
|
||||
assert_eq!(gma.update(1.0), None);
|
||||
assert_eq!(gma.update(4.0), None);
|
||||
// GMA(3) of [1, 4, 2] = (1·4·2)^(1/3) = 8^(1/3) = 2.
|
||||
assert_relative_eq!(gma.update(2.0).unwrap(), 2.0, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn known_value_period_2() {
|
||||
// GMA(2) of [4, 9] = sqrt(36) = 6.
|
||||
let mut gma = GeometricMa::new(2).unwrap();
|
||||
let v = gma.batch(&[4.0, 9.0]);
|
||||
assert_relative_eq!(v[1].unwrap(), 6.0, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn constant_series_returns_the_constant() {
|
||||
let mut gma = GeometricMa::new(5).unwrap();
|
||||
for v in gma.batch(&[42.0; 20]).into_iter().flatten() {
|
||||
assert_relative_eq!(v, 42.0, epsilon = 1e-9);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn period_one_is_pass_through() {
|
||||
let mut gma = GeometricMa::new(1).unwrap();
|
||||
assert_relative_eq!(gma.update(5.5).unwrap(), 5.5, epsilon = 1e-12);
|
||||
assert_relative_eq!(gma.update(7.5).unwrap(), 7.5, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn below_or_equal_arithmetic_mean() {
|
||||
// The geometric mean never exceeds the arithmetic mean of the same set.
|
||||
let mut gma = GeometricMa::new(4).unwrap();
|
||||
let prices = [10.0, 20.0, 5.0, 40.0];
|
||||
let g = gma.batch(&prices)[3].unwrap();
|
||||
let arithmetic = prices.iter().sum::<f64>() / 4.0;
|
||||
assert!(
|
||||
g < arithmetic,
|
||||
"geometric {g} should be below arithmetic {arithmetic}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn matches_naive_over_inputs() {
|
||||
let prices: Vec<f64> = (1..=30).map(|i| f64::from(i) * 1.7 + 1.0).collect();
|
||||
let mut gma = GeometricMa::new(7).unwrap();
|
||||
let got = gma.batch(&prices);
|
||||
let want = gma_naive(&prices, 7);
|
||||
for (i, (g, w)) in got.iter().zip(want.iter()).enumerate() {
|
||||
assert_eq!(g.is_some(), w.is_some(), "warmup mismatch at index {i}");
|
||||
if let (Some(a), Some(b)) = (g, w) {
|
||||
assert_relative_eq!(*a, *b, epsilon = 1e-9);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut gma = GeometricMa::new(4).unwrap();
|
||||
gma.batch(&[1.0, 2.0, 3.0, 4.0, 5.0]);
|
||||
assert!(gma.is_ready());
|
||||
gma.reset();
|
||||
assert!(!gma.is_ready());
|
||||
assert_eq!(gma.update(10.0), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let prices: Vec<f64> = (1..=20).map(|i| f64::from(i) * 0.5 + 1.0).collect();
|
||||
let mut a = GeometricMa::new(5).unwrap();
|
||||
let mut b = GeometricMa::new(5).unwrap();
|
||||
assert_eq!(
|
||||
a.batch(&prices),
|
||||
prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_non_finite_and_non_positive_input() {
|
||||
let mut gma = GeometricMa::new(3).unwrap();
|
||||
gma.update(1.0);
|
||||
gma.update(4.0);
|
||||
let ready = gma.update(2.0).expect("GMA(3) ready after three inputs");
|
||||
// Non-finite and non-positive inputs are skipped (geometric mean needs
|
||||
// strictly positive values) and the window is left unchanged.
|
||||
assert_eq!(gma.update(f64::NAN), Some(ready));
|
||||
assert_eq!(gma.update(0.0), Some(ready));
|
||||
assert_eq!(gma.update(-3.0), Some(ready));
|
||||
// The window still holds 1, 4, 2 -> next real input slides it to 4, 2, 16.
|
||||
let want = (4.0_f64 * 2.0 * 16.0).powf(1.0 / 3.0);
|
||||
assert_relative_eq!(gma.update(16.0).unwrap(), want, epsilon = 1e-9);
|
||||
}
|
||||
|
||||
proptest::proptest! {
|
||||
#![proptest_config(proptest::test_runner::Config::with_cases(48))]
|
||||
#[test]
|
||||
fn proptest_matches_naive(
|
||||
period in 1usize..15,
|
||||
prices in proptest::collection::vec(0.01_f64..1000.0, 0..120),
|
||||
) {
|
||||
let mut gma = GeometricMa::new(period).unwrap();
|
||||
let got = gma.batch(&prices);
|
||||
let want = gma_naive(&prices, period);
|
||||
proptest::prop_assert_eq!(got.len(), want.len());
|
||||
for (g, w) in got.iter().zip(want.iter()) {
|
||||
match (g, w) {
|
||||
(None, None) => {}
|
||||
(Some(a), Some(b)) => proptest::prop_assert!(
|
||||
(a - b).abs() <= 1e-6 * b.abs().max(1.0),
|
||||
"got={a} want={b}"
|
||||
),
|
||||
_ => proptest::prop_assert!(false, "warmup mismatch"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
//! Holt's linear (double exponential) smoothing.
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Holt's linear method — double exponential smoothing with a level and a
|
||||
/// trend component.
|
||||
///
|
||||
/// A single [`Ema`](crate::Ema) tracks only a *level* and therefore lags any
|
||||
/// sustained trend. Holt's method adds a second smoothed state, the trend, and
|
||||
/// reports the one-step-ahead forecast `level + trend`, which removes that lag
|
||||
/// on trending data while still smoothing noise.
|
||||
///
|
||||
/// ```text
|
||||
/// level_t = α · price_t + (1 − α) · (level_{t-1} + trend_{t-1})
|
||||
/// trend_t = β · (level_t − level_{t-1}) + (1 − β) · trend_{t-1}
|
||||
/// output = level_t + trend_t (one-step-ahead forecast)
|
||||
/// ```
|
||||
///
|
||||
/// `α ∈ (0, 1]` is the level smoothing constant and `β ∈ (0, 1]` the trend
|
||||
/// smoothing constant. The state is seeded from the first two inputs
|
||||
/// (`level = price_1`, `trend = price_1 − price_0`), so the first output lands
|
||||
/// on the **second** input.
|
||||
///
|
||||
/// On a perfectly linear series the forecast is exact from the second bar
|
||||
/// onward (for any `α`, `β`): if the level equals the current value and the
|
||||
/// trend equals the slope, both invariants are preserved and `level + trend`
|
||||
/// equals the next value.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{HoltWinters, Indicator};
|
||||
///
|
||||
/// let mut indicator = HoltWinters::new(0.2, 0.1).unwrap();
|
||||
/// let mut last = None;
|
||||
/// for i in 0..80 {
|
||||
/// last = indicator.update(100.0 + f64::from(i));
|
||||
/// }
|
||||
/// assert!(last.is_some());
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HoltWinters {
|
||||
alpha: f64,
|
||||
beta: f64,
|
||||
/// `(level, trend)` once seeded.
|
||||
state: Option<(f64, f64)>,
|
||||
/// First input, held until the second arrives to seed the trend.
|
||||
prev_price: Option<f64>,
|
||||
}
|
||||
|
||||
impl HoltWinters {
|
||||
/// Construct Holt's linear smoother with level constant `alpha` and trend
|
||||
/// constant `beta`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::InvalidPeriod`] if either constant is non-finite or
|
||||
/// outside `(0.0, 1.0]`.
|
||||
pub fn new(alpha: f64, beta: f64) -> Result<Self> {
|
||||
if !alpha.is_finite() || alpha <= 0.0 || alpha > 1.0 {
|
||||
return Err(Error::InvalidPeriod {
|
||||
message: "HoltWinters alpha must be in (0.0, 1.0]",
|
||||
});
|
||||
}
|
||||
if !beta.is_finite() || beta <= 0.0 || beta > 1.0 {
|
||||
return Err(Error::InvalidPeriod {
|
||||
message: "HoltWinters beta must be in (0.0, 1.0]",
|
||||
});
|
||||
}
|
||||
Ok(Self {
|
||||
alpha,
|
||||
beta,
|
||||
state: None,
|
||||
prev_price: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Level smoothing constant `alpha`.
|
||||
pub const fn alpha(&self) -> f64 {
|
||||
self.alpha
|
||||
}
|
||||
|
||||
/// Trend smoothing constant `beta`.
|
||||
pub const fn beta(&self) -> f64 {
|
||||
self.beta
|
||||
}
|
||||
|
||||
/// Current smoothed level, if seeded.
|
||||
pub fn level(&self) -> Option<f64> {
|
||||
self.state.map(|(level, _)| level)
|
||||
}
|
||||
|
||||
/// Current smoothed trend, if seeded.
|
||||
pub fn trend(&self) -> Option<f64> {
|
||||
self.state.map(|(_, trend)| trend)
|
||||
}
|
||||
|
||||
/// Current one-step-ahead forecast `level + trend`, if seeded.
|
||||
pub fn value(&self) -> Option<f64> {
|
||||
self.state.map(|(level, trend)| level + trend)
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for HoltWinters {
|
||||
type Input = f64;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, price: f64) -> Option<f64> {
|
||||
if !price.is_finite() {
|
||||
return self.value();
|
||||
}
|
||||
match self.state {
|
||||
None => {
|
||||
if let Some(prev) = self.prev_price {
|
||||
// Second input: seed level and trend.
|
||||
let level = price;
|
||||
let trend = price - prev;
|
||||
self.state = Some((level, trend));
|
||||
Some(level + trend)
|
||||
} else {
|
||||
// First input: hold it to seed the trend next time.
|
||||
self.prev_price = Some(price);
|
||||
None
|
||||
}
|
||||
}
|
||||
Some((level, trend)) => {
|
||||
let level_new = self.alpha * price + (1.0 - self.alpha) * (level + trend);
|
||||
let trend_new = self.beta * (level_new - level) + (1.0 - self.beta) * trend;
|
||||
self.state = Some((level_new, trend_new));
|
||||
Some(level_new + trend_new)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.state = None;
|
||||
self.prev_price = None;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
// Two inputs are needed to seed the level and the trend.
|
||||
2
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.state.is_some()
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"HoltWinters"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
/// Independent reference for the steady-state recurrence.
|
||||
fn naive(prices: &[f64], alpha: f64, beta: f64) -> Vec<Option<f64>> {
|
||||
let mut state: Option<(f64, f64)> = None;
|
||||
let mut prev: Option<f64> = None;
|
||||
let mut out = Vec::with_capacity(prices.len());
|
||||
for &price in prices {
|
||||
let v = match state {
|
||||
None => {
|
||||
if let Some(p0) = prev {
|
||||
let level = price;
|
||||
let trend = price - p0;
|
||||
state = Some((level, trend));
|
||||
Some(level + trend)
|
||||
} else {
|
||||
prev = Some(price);
|
||||
None
|
||||
}
|
||||
}
|
||||
Some((level, trend)) => {
|
||||
let ln = alpha * price + (1.0 - alpha) * (level + trend);
|
||||
let tn = beta * (ln - level) + (1.0 - beta) * trend;
|
||||
state = Some((ln, tn));
|
||||
Some(ln + tn)
|
||||
}
|
||||
};
|
||||
out.push(v);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_invalid_alpha() {
|
||||
assert!(matches!(
|
||||
HoltWinters::new(0.0, 0.1),
|
||||
Err(Error::InvalidPeriod { .. })
|
||||
));
|
||||
assert!(matches!(
|
||||
HoltWinters::new(1.5, 0.1),
|
||||
Err(Error::InvalidPeriod { .. })
|
||||
));
|
||||
assert!(matches!(
|
||||
HoltWinters::new(f64::NAN, 0.1),
|
||||
Err(Error::InvalidPeriod { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_invalid_beta() {
|
||||
assert!(matches!(
|
||||
HoltWinters::new(0.2, 0.0),
|
||||
Err(Error::InvalidPeriod { .. })
|
||||
));
|
||||
assert!(matches!(
|
||||
HoltWinters::new(0.2, 1.5),
|
||||
Err(Error::InvalidPeriod { .. })
|
||||
));
|
||||
assert!(matches!(
|
||||
HoltWinters::new(0.2, f64::INFINITY),
|
||||
Err(Error::InvalidPeriod { .. })
|
||||
));
|
||||
}
|
||||
|
||||
/// Cover the const accessors `alpha` + `beta` and the Indicator-impl
|
||||
/// `warmup_period` + `name`.
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let hw = HoltWinters::new(0.2, 0.1).unwrap();
|
||||
assert_relative_eq!(hw.alpha(), 0.2, epsilon = 1e-12);
|
||||
assert_relative_eq!(hw.beta(), 0.1, epsilon = 1e-12);
|
||||
assert_eq!(hw.warmup_period(), 2);
|
||||
assert_eq!(hw.name(), "HoltWinters");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn warmup_then_seed_on_second_input() {
|
||||
let mut hw = HoltWinters::new(0.2, 0.1).unwrap();
|
||||
assert_eq!(hw.update(10.0), None);
|
||||
// Second input seeds level = 12, trend = 12 - 10 = 2 -> forecast 14.
|
||||
assert_relative_eq!(hw.update(12.0).unwrap(), 14.0, epsilon = 1e-12);
|
||||
assert_relative_eq!(hw.level().unwrap(), 12.0, epsilon = 1e-12);
|
||||
assert_relative_eq!(hw.trend().unwrap(), 2.0, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn linear_series_forecasts_exactly() {
|
||||
// On a perfect ramp the one-step forecast equals the next value, for
|
||||
// any alpha/beta, from the second bar onward.
|
||||
let prices: Vec<f64> = (1..=20).map(f64::from).collect();
|
||||
let mut hw = HoltWinters::new(0.3, 0.4).unwrap();
|
||||
let out = hw.batch(&prices);
|
||||
assert!(out[0].is_none());
|
||||
for (i, v) in out.iter().enumerate().skip(1) {
|
||||
// forecast at index i is the price at index i + 1 = (i + 2).
|
||||
assert_relative_eq!(v.unwrap(), (i + 2) as f64, epsilon = 1e-9);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn constant_series_yields_constant() {
|
||||
let mut hw = HoltWinters::new(0.2, 0.1).unwrap();
|
||||
let out = hw.batch(&[42.0_f64; 30]);
|
||||
for v in out.into_iter().skip(1).flatten() {
|
||||
assert_relative_eq!(v, 42.0, epsilon = 1e-9);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn matches_naive_recurrence() {
|
||||
let prices: Vec<f64> = (0..60)
|
||||
.map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 10.0 + f64::from(i) * 0.2)
|
||||
.collect();
|
||||
let mut hw = HoltWinters::new(0.25, 0.15).unwrap();
|
||||
let got = hw.batch(&prices);
|
||||
let want = naive(&prices, 0.25, 0.15);
|
||||
for (g, w) in got.iter().zip(want.iter()) {
|
||||
assert_eq!(g.is_some(), w.is_some());
|
||||
if let (Some(a), Some(b)) = (g, w) {
|
||||
assert_relative_eq!(a, b, epsilon = 1e-9);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut hw = HoltWinters::new(0.2, 0.1).unwrap();
|
||||
hw.batch(&(1..=20).map(f64::from).collect::<Vec<_>>());
|
||||
assert!(hw.is_ready());
|
||||
hw.reset();
|
||||
assert!(!hw.is_ready());
|
||||
assert_eq!(hw.update(1.0), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let prices: Vec<f64> = (1..=30).map(|i| f64::from(i) * 0.5).collect();
|
||||
let mut a = HoltWinters::new(0.3, 0.2).unwrap();
|
||||
let mut b = HoltWinters::new(0.3, 0.2).unwrap();
|
||||
assert_eq!(
|
||||
a.batch(&prices),
|
||||
prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_non_finite_input() {
|
||||
let mut hw = HoltWinters::new(0.2, 0.1).unwrap();
|
||||
// Non-finite before any state returns None.
|
||||
assert_eq!(hw.update(f64::NAN), None);
|
||||
hw.update(10.0);
|
||||
let ready = hw.update(12.0).expect("seeded on second finite input");
|
||||
// Non-finite after seeding returns the current forecast unchanged.
|
||||
assert_eq!(hw.update(f64::NAN), Some(ready));
|
||||
assert_eq!(hw.update(f64::INFINITY), Some(ready));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
//! Median Moving Average.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Median Moving Average — the rolling median of the last `period` inputs.
|
||||
///
|
||||
/// For an odd `period` the output is the middle order statistic of the window;
|
||||
/// for an even `period` it is the average of the two central values. Because it
|
||||
/// is a rank statistic rather than a sum, the median MA is far more robust to
|
||||
/// single outliers than the [`Sma`](crate::Sma): a lone spike shifts the rank
|
||||
/// by at most one position instead of dragging the whole average.
|
||||
///
|
||||
/// Each `update` slides the window and computes the median by sorting a copy of
|
||||
/// the `period` buffered values — O(`period` · log `period`) per step, with the
|
||||
/// period fixed and bounded.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Indicator, MedianMa};
|
||||
///
|
||||
/// let mut indicator = MedianMa::new(5).unwrap();
|
||||
/// let mut last = None;
|
||||
/// for i in 0..80 {
|
||||
/// last = indicator.update(100.0 + f64::from(i));
|
||||
/// }
|
||||
/// assert!(last.is_some());
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MedianMa {
|
||||
period: usize,
|
||||
window: VecDeque<f64>,
|
||||
}
|
||||
|
||||
impl MedianMa {
|
||||
/// Construct a new median moving average over `period` inputs.
|
||||
///
|
||||
/// # 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),
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured period.
|
||||
pub const fn period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
|
||||
/// Current value if the window is full.
|
||||
pub fn value(&self) -> Option<f64> {
|
||||
if self.window.len() != self.period {
|
||||
return None;
|
||||
}
|
||||
let mut sorted: Vec<f64> = self.window.iter().copied().collect();
|
||||
sorted.sort_by(|a, b| a.partial_cmp(b).expect("window holds only finite values"));
|
||||
let mid = self.period / 2;
|
||||
if self.period % 2 == 1 {
|
||||
Some(sorted[mid])
|
||||
} else {
|
||||
Some(f64::midpoint(sorted[mid - 1], sorted[mid]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for MedianMa {
|
||||
type Input = f64;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, input: f64) -> Option<f64> {
|
||||
if !input.is_finite() {
|
||||
return self.value();
|
||||
}
|
||||
if self.window.len() == self.period {
|
||||
self.window.pop_front();
|
||||
}
|
||||
self.window.push_back(input);
|
||||
self.value()
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.window.clear();
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.window.len() == self.period
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"MedianMA"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
#[test]
|
||||
fn new_rejects_zero_period() {
|
||||
assert!(matches!(MedianMa::new(0), Err(Error::PeriodZero)));
|
||||
}
|
||||
|
||||
/// Cover the const accessor `period` and the Indicator-impl `warmup_period`
|
||||
/// + `name`.
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let mma = MedianMa::new(7).unwrap();
|
||||
assert_eq!(mma.period(), 7);
|
||||
assert_eq!(mma.warmup_period(), 7);
|
||||
assert_eq!(mma.name(), "MedianMA");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn warmup_returns_none_then_odd_median() {
|
||||
let mut mma = MedianMa::new(3).unwrap();
|
||||
assert_eq!(mma.update(5.0), None);
|
||||
assert_eq!(mma.update(1.0), None);
|
||||
// median of [5, 1, 3] = 3 (middle order statistic).
|
||||
assert_relative_eq!(mma.update(3.0).unwrap(), 3.0, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn even_period_averages_two_central_values() {
|
||||
// median of [1, 2, 3, 4] = (2 + 3) / 2 = 2.5.
|
||||
let mut mma = MedianMa::new(4).unwrap();
|
||||
let v = mma.batch(&[1.0, 2.0, 3.0, 4.0]);
|
||||
assert_relative_eq!(v[3].unwrap(), 2.5, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn robust_to_single_outlier() {
|
||||
// A lone spike does not move the median of an odd window the way it
|
||||
// would move an SMA. median of [10, 11, 9999] = 11.
|
||||
let mut mma = MedianMa::new(3).unwrap();
|
||||
let v = mma.batch(&[10.0, 11.0, 9999.0]);
|
||||
assert_relative_eq!(v[2].unwrap(), 11.0, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn period_one_is_pass_through() {
|
||||
let mut mma = MedianMa::new(1).unwrap();
|
||||
assert_relative_eq!(mma.update(5.5).unwrap(), 5.5, epsilon = 1e-12);
|
||||
assert_relative_eq!(mma.update(7.5).unwrap(), 7.5, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slides_window_correctly() {
|
||||
// After [1,2,3] the window slides to [2,3,4] -> median 3, then [3,4,5] -> 4.
|
||||
let mut mma = MedianMa::new(3).unwrap();
|
||||
let v = mma.batch(&[1.0, 2.0, 3.0, 4.0, 5.0]);
|
||||
assert_relative_eq!(v[2].unwrap(), 2.0, epsilon = 1e-12);
|
||||
assert_relative_eq!(v[3].unwrap(), 3.0, epsilon = 1e-12);
|
||||
assert_relative_eq!(v[4].unwrap(), 4.0, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut mma = MedianMa::new(4).unwrap();
|
||||
mma.batch(&[1.0, 2.0, 3.0, 4.0, 5.0]);
|
||||
assert!(mma.is_ready());
|
||||
mma.reset();
|
||||
assert!(!mma.is_ready());
|
||||
assert_eq!(mma.update(10.0), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let prices: Vec<f64> = (1..=20).map(|i| (f64::from(i) * 0.7).sin() * 5.0).collect();
|
||||
let mut a = MedianMa::new(5).unwrap();
|
||||
let mut b = MedianMa::new(5).unwrap();
|
||||
assert_eq!(
|
||||
a.batch(&prices),
|
||||
prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_non_finite_input_but_keeps_state() {
|
||||
let mut mma = MedianMa::new(3).unwrap();
|
||||
mma.update(5.0);
|
||||
mma.update(1.0);
|
||||
let ready = mma
|
||||
.update(3.0)
|
||||
.expect("MedianMA(3) ready after three inputs");
|
||||
assert_eq!(mma.update(f64::NAN), Some(ready));
|
||||
assert_eq!(mma.update(f64::INFINITY), Some(ready));
|
||||
// Window still [5, 1, 3] -> next real input slides to [1, 3, 8] -> median 3.
|
||||
assert_relative_eq!(mma.update(8.0).unwrap(), 3.0, epsilon = 1e-12);
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ mod accelerator_oscillator;
|
||||
mod ad_oscillator;
|
||||
mod ad_volume_line;
|
||||
mod adaptive_cycle;
|
||||
mod adaptive_laguerre_filter;
|
||||
mod adl;
|
||||
mod advance_block;
|
||||
mod advance_decline;
|
||||
@@ -106,6 +107,7 @@ mod dx;
|
||||
mod ease_of_movement;
|
||||
mod effective_spread;
|
||||
mod ehlers_stochastic;
|
||||
mod ehma;
|
||||
mod elder_impulse;
|
||||
mod ema;
|
||||
mod empirical_mode_decomposition;
|
||||
@@ -138,6 +140,8 @@ mod gain_loss_ratio;
|
||||
mod gap_side_by_side_white;
|
||||
mod garman_klass;
|
||||
mod gartley;
|
||||
mod generalized_dema;
|
||||
mod geometric_ma;
|
||||
mod golden_pocket;
|
||||
mod granger_causality;
|
||||
mod gravestone_doji;
|
||||
@@ -155,6 +159,7 @@ mod hilbert_dominant_cycle;
|
||||
mod hilo_activator;
|
||||
mod historical_volatility;
|
||||
mod hma;
|
||||
mod holt_winters;
|
||||
mod homing_pigeon;
|
||||
mod ht_dcphase;
|
||||
mod ht_phasor;
|
||||
@@ -212,6 +217,7 @@ mod mcclellan_oscillator;
|
||||
mod mcclellan_summation_index;
|
||||
mod mcginley_dynamic;
|
||||
mod median_absolute_deviation;
|
||||
mod median_ma;
|
||||
mod median_price;
|
||||
mod mfi;
|
||||
mod microprice;
|
||||
@@ -298,6 +304,7 @@ mod shooting_star;
|
||||
mod short_line;
|
||||
mod signed_volume;
|
||||
mod sine_wave;
|
||||
mod sine_weighted_ma;
|
||||
mod skewness;
|
||||
mod sma;
|
||||
mod smi;
|
||||
@@ -413,6 +420,7 @@ pub use accelerator_oscillator::AcceleratorOscillator;
|
||||
pub use ad_oscillator::AdOscillator;
|
||||
pub use ad_volume_line::AdVolumeLine;
|
||||
pub use adaptive_cycle::AdaptiveCycle;
|
||||
pub use adaptive_laguerre_filter::AdaptiveLaguerreFilter;
|
||||
pub use adl::Adl;
|
||||
pub use advance_block::AdvanceBlock;
|
||||
pub use advance_decline::AdvanceDecline;
|
||||
@@ -502,6 +510,7 @@ pub use dx::Dx;
|
||||
pub use ease_of_movement::EaseOfMovement;
|
||||
pub use effective_spread::EffectiveSpread;
|
||||
pub use ehlers_stochastic::EhlersStochastic;
|
||||
pub use ehma::Ehma;
|
||||
pub use elder_impulse::ElderImpulse;
|
||||
pub use ema::Ema;
|
||||
pub use empirical_mode_decomposition::EmpiricalModeDecomposition;
|
||||
@@ -534,6 +543,8 @@ pub use gain_loss_ratio::GainLossRatio;
|
||||
pub use gap_side_by_side_white::GapSideBySideWhite;
|
||||
pub use garman_klass::GarmanKlassVolatility;
|
||||
pub use gartley::Gartley;
|
||||
pub use generalized_dema::GeneralizedDema;
|
||||
pub use geometric_ma::GeometricMa;
|
||||
pub use golden_pocket::{GoldenPocket, GoldenPocketOutput};
|
||||
pub use granger_causality::GrangerCausality;
|
||||
pub use gravestone_doji::GravestoneDoji;
|
||||
@@ -551,6 +562,7 @@ pub use hilbert_dominant_cycle::HilbertDominantCycle;
|
||||
pub use hilo_activator::HiLoActivator;
|
||||
pub use historical_volatility::HistoricalVolatility;
|
||||
pub use hma::Hma;
|
||||
pub use holt_winters::HoltWinters;
|
||||
pub use homing_pigeon::HomingPigeon;
|
||||
pub use ht_dcphase::HtDcPhase;
|
||||
pub use ht_phasor::{HtPhasor, HtPhasorOutput};
|
||||
@@ -608,6 +620,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_ma::MedianMa;
|
||||
pub use median_price::MedianPrice;
|
||||
pub use mfi::Mfi;
|
||||
pub use microprice::Microprice;
|
||||
@@ -694,6 +707,7 @@ pub use shooting_star::ShootingStar;
|
||||
pub use short_line::ShortLine;
|
||||
pub use signed_volume::SignedVolume;
|
||||
pub use sine_wave::SineWave;
|
||||
pub use sine_weighted_ma::SineWeightedMa;
|
||||
pub use skewness::Skewness;
|
||||
pub use sma::Sma;
|
||||
pub use smi::Smi;
|
||||
@@ -830,6 +844,13 @@ pub const FAMILIES: &[(&str, &[&str])] = &[
|
||||
"Jma",
|
||||
"Alligator",
|
||||
"Evwma",
|
||||
"SineWeightedMa",
|
||||
"GeometricMa",
|
||||
"Ehma",
|
||||
"MedianMa",
|
||||
"AdaptiveLaguerreFilter",
|
||||
"GeneralizedDema",
|
||||
"HoltWinters",
|
||||
],
|
||||
),
|
||||
(
|
||||
@@ -1342,6 +1363,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, 396, "FAMILIES total drifted from indicator count");
|
||||
assert_eq!(total, 403, "FAMILIES total drifted from indicator count");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
//! Sine-Weighted Moving Average (SWMA).
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Sine-Weighted Moving Average — a windowed average whose weights follow one
|
||||
/// half-cycle of a sine wave.
|
||||
///
|
||||
/// Over the last `period` inputs the weight of the value at position
|
||||
/// `i = 0, 1, …, period − 1` (oldest to newest) is
|
||||
///
|
||||
/// ```text
|
||||
/// w_i = sin(π · (i + 1) / (period + 1))
|
||||
/// SWMA = Σ (w_i · value_i) / Σ w_i
|
||||
/// ```
|
||||
///
|
||||
/// The window is symmetric: weights rise to a peak in the middle of the window
|
||||
/// and fall off at both ends, so the central observations dominate while the
|
||||
/// extremes are de-emphasised. Every weight is strictly positive because the
|
||||
/// argument `(i + 1) / (period + 1)` lies in the open interval `(0, 1)`, so the
|
||||
/// normaliser is always non-zero.
|
||||
///
|
||||
/// Each `update` is O(`period`): the fixed weight vector is dotted with the
|
||||
/// trailing window, mirroring the way [`Alma`](crate::Alma) recomputes its
|
||||
/// Gaussian weights. `period == 1` collapses to a pass-through
|
||||
/// (`w_0 = sin(π/2) = 1`).
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Indicator, SineWeightedMa};
|
||||
///
|
||||
/// let mut indicator = SineWeightedMa::new(5).unwrap();
|
||||
/// let mut last = None;
|
||||
/// for i in 0..80 {
|
||||
/// last = indicator.update(100.0 + f64::from(i));
|
||||
/// }
|
||||
/// assert!(last.is_some());
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SineWeightedMa {
|
||||
period: usize,
|
||||
window: VecDeque<f64>,
|
||||
/// Sine weights for positions `0..period` (oldest to newest), constant in
|
||||
/// `period`.
|
||||
weights: Vec<f64>,
|
||||
weights_total: f64,
|
||||
}
|
||||
|
||||
impl SineWeightedMa {
|
||||
/// Construct a new sine-weighted moving average over `period` inputs.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::PeriodZero`] if `period == 0`.
|
||||
pub fn new(period: usize) -> Result<Self> {
|
||||
if period == 0 {
|
||||
return Err(Error::PeriodZero);
|
||||
}
|
||||
let denom = period as f64 + 1.0;
|
||||
let weights: Vec<f64> = (0..period)
|
||||
.map(|i| (std::f64::consts::PI * (i as f64 + 1.0) / denom).sin())
|
||||
.collect();
|
||||
let weights_total = weights.iter().sum();
|
||||
Ok(Self {
|
||||
period,
|
||||
window: VecDeque::with_capacity(period),
|
||||
weights,
|
||||
weights_total,
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured period.
|
||||
pub const fn period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
|
||||
/// Current value if the window is full.
|
||||
pub fn value(&self) -> Option<f64> {
|
||||
if self.window.len() == self.period {
|
||||
let dot: f64 = self
|
||||
.window
|
||||
.iter()
|
||||
.zip(&self.weights)
|
||||
.map(|(v, w)| v * w)
|
||||
.sum();
|
||||
Some(dot / self.weights_total)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for SineWeightedMa {
|
||||
type Input = f64;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, input: f64) -> Option<f64> {
|
||||
if !input.is_finite() {
|
||||
return self.value();
|
||||
}
|
||||
if self.window.len() == self.period {
|
||||
self.window.pop_front();
|
||||
}
|
||||
self.window.push_back(input);
|
||||
self.value()
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.window.clear();
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.window.len() == self.period
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"SWMA"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
/// Reference implementation: explicit sine-weighted average over a window.
|
||||
fn swma_naive(prices: &[f64], period: usize) -> Vec<Option<f64>> {
|
||||
let denom = period as f64 + 1.0;
|
||||
let weights: Vec<f64> = (0..period)
|
||||
.map(|i| (std::f64::consts::PI * (i as f64 + 1.0) / denom).sin())
|
||||
.collect();
|
||||
let total: f64 = weights.iter().sum();
|
||||
prices
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, _)| {
|
||||
if i + 1 < period {
|
||||
None
|
||||
} else {
|
||||
let window = &prices[i + 1 - period..=i];
|
||||
let dot: f64 = window.iter().zip(&weights).map(|(v, w)| v * w).sum();
|
||||
Some(dot / total)
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_rejects_zero_period() {
|
||||
assert!(matches!(SineWeightedMa::new(0), Err(Error::PeriodZero)));
|
||||
}
|
||||
|
||||
/// Cover the const accessor `period` and the Indicator-impl `warmup_period`
|
||||
/// + `name`.
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let swma = SineWeightedMa::new(7).unwrap();
|
||||
assert_eq!(swma.period(), 7);
|
||||
assert_eq!(swma.warmup_period(), 7);
|
||||
assert_eq!(swma.name(), "SWMA");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn warmup_returns_none() {
|
||||
let mut swma = SineWeightedMa::new(3).unwrap();
|
||||
assert_eq!(swma.update(1.0), None);
|
||||
assert_eq!(swma.update(2.0), None);
|
||||
// SWMA(3): weights sin(pi/4), sin(pi/2), sin(3pi/4) = [√½, 1, √½].
|
||||
// Over [1,2,3]: (√½·1 + 1·2 + √½·3) / (√½ + 1 + √½).
|
||||
let s = std::f64::consts::FRAC_1_SQRT_2;
|
||||
let total = s + 1.0 + s;
|
||||
let want = (s * 1.0 + 1.0 * 2.0 + s * 3.0) / total;
|
||||
assert_relative_eq!(swma.update(3.0).unwrap(), want, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn symmetric_weights_give_midpoint_on_linear_window() {
|
||||
// For a perfectly linear window the symmetric weighting reproduces the
|
||||
// arithmetic centre of the window.
|
||||
let mut swma = SineWeightedMa::new(5).unwrap();
|
||||
let v = swma.batch(&[1.0, 2.0, 3.0, 4.0, 5.0]);
|
||||
assert_relative_eq!(v[4].unwrap(), 3.0, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn period_one_is_pass_through() {
|
||||
let mut swma = SineWeightedMa::new(1).unwrap();
|
||||
assert_relative_eq!(swma.update(5.5).unwrap(), 5.5, epsilon = 1e-12);
|
||||
assert_relative_eq!(swma.update(7.5).unwrap(), 7.5, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn matches_naive_over_inputs() {
|
||||
let prices: Vec<f64> = (1..=30).map(|i| f64::from(i) * 1.7 - 5.0).collect();
|
||||
let mut swma = SineWeightedMa::new(7).unwrap();
|
||||
let got = swma.batch(&prices);
|
||||
let want = swma_naive(&prices, 7);
|
||||
for (i, (g, w)) in got.iter().zip(want.iter()).enumerate() {
|
||||
assert_eq!(g.is_some(), w.is_some(), "warmup mismatch at index {i}");
|
||||
if let (Some(a), Some(b)) = (g, w) {
|
||||
assert_relative_eq!(*a, *b, epsilon = 1e-9);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut swma = SineWeightedMa::new(4).unwrap();
|
||||
swma.batch(&[1.0, 2.0, 3.0, 4.0, 5.0]);
|
||||
assert!(swma.is_ready());
|
||||
swma.reset();
|
||||
assert!(!swma.is_ready());
|
||||
assert_eq!(swma.update(10.0), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let prices: Vec<f64> = (1..=20).map(|i| f64::from(i) * 0.5).collect();
|
||||
let mut a = SineWeightedMa::new(5).unwrap();
|
||||
let mut b = SineWeightedMa::new(5).unwrap();
|
||||
assert_eq!(
|
||||
a.batch(&prices),
|
||||
prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_non_finite_input_but_keeps_state() {
|
||||
let mut swma = SineWeightedMa::new(3).unwrap();
|
||||
swma.update(1.0);
|
||||
swma.update(2.0);
|
||||
let ready = swma.update(3.0).expect("SWMA(3) ready after three inputs");
|
||||
assert_eq!(swma.update(f64::NAN), Some(ready));
|
||||
assert_eq!(swma.update(f64::INFINITY), Some(ready));
|
||||
// The window still holds 1, 2, 3 -> next real input slides it to 2, 3, 4.
|
||||
let s = std::f64::consts::FRAC_1_SQRT_2;
|
||||
let total = s + 1.0 + s;
|
||||
let want = (s * 2.0 + 1.0 * 3.0 + s * 4.0) / total;
|
||||
assert_relative_eq!(swma.update(4.0).unwrap(), want, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
proptest::proptest! {
|
||||
#![proptest_config(proptest::test_runner::Config::with_cases(48))]
|
||||
#[test]
|
||||
fn proptest_matches_naive(
|
||||
period in 1usize..15,
|
||||
prices in proptest::collection::vec(-500.0_f64..500.0, 0..120),
|
||||
) {
|
||||
let mut swma = SineWeightedMa::new(period).unwrap();
|
||||
let got = swma.batch(&prices);
|
||||
let want = swma_naive(&prices, period);
|
||||
proptest::prop_assert_eq!(got.len(), want.len());
|
||||
for (g, w) in got.iter().zip(want.iter()) {
|
||||
match (g, w) {
|
||||
(None, None) => {}
|
||||
(Some(a), Some(b)) => proptest::prop_assert!(
|
||||
(a - b).abs() < 1e-7,
|
||||
"got={a} want={b}"
|
||||
),
|
||||
_ => proptest::prop_assert!(false, "warmup mismatch"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -57,37 +57,39 @@ pub use derivatives::DerivativesTick;
|
||||
pub use error::{Error, Result};
|
||||
pub use indicators::{
|
||||
AbandonedBaby, Abcd, AbsoluteBreadthIndex, AccelerationBands, AccelerationBandsOutput,
|
||||
AcceleratorOscillator, AdOscillator, AdVolumeLine, AdaptiveCycle, Adl, AdvanceBlock,
|
||||
AdvanceDecline, AdvanceDeclineRatio, Adx, AdxOutput, Adxr, Alligator, AlligatorOutput, Alma,
|
||||
Alpha, AmihudIlliquidity, AnchoredRsi, AnchoredVwap, Apo, Aroon, AroonOscillator, AroonOutput,
|
||||
Atr, AtrBands, AtrBandsOutput, AtrTrailingStop, AutoFib, AutoFibOutput, Autocorrelation,
|
||||
AverageDailyRange, AverageDrawdown, AvgPrice, AwesomeOscillator, AwesomeOscillatorHistogram,
|
||||
BalanceOfPower, Bat, BeltHold, Beta, BetaNeutralSpread, 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,
|
||||
AcceleratorOscillator, AdOscillator, AdVolumeLine, AdaptiveCycle, AdaptiveLaguerreFilter, Adl,
|
||||
AdvanceBlock, AdvanceDecline, AdvanceDeclineRatio, Adx, AdxOutput, Adxr, Alligator,
|
||||
AlligatorOutput, Alma, Alpha, AmihudIlliquidity, AnchoredRsi, AnchoredVwap, Apo, Aroon,
|
||||
AroonOscillator, AroonOutput, Atr, AtrBands, AtrBandsOutput, AtrTrailingStop, AutoFib,
|
||||
AutoFibOutput, Autocorrelation, AverageDailyRange, AverageDrawdown, AvgPrice,
|
||||
AwesomeOscillator, AwesomeOscillatorHistogram, BalanceOfPower, Bat, BeltHold, Beta,
|
||||
BetaNeutralSpread, 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, DetrendedStdDev, DistanceSsd, Doji, DojiStar,
|
||||
Donchian, DonchianOutput, DonchianStop, DonchianStopOutput, DoubleBollinger,
|
||||
DoubleBollingerOutput, DoubleTopBottom, DownsideGapThreeMethods, Dpo, DragonflyDoji,
|
||||
DrawdownDuration, Dx, EaseOfMovement, EffectiveSpread, EhlersStochastic, ElderImpulse, Ema,
|
||||
EmpiricalModeDecomposition, Engulfing, EveningDojiStar, Evwma, Expectancy, FallingThreeMethods,
|
||||
Fama, FibArcs, FibArcsOutput, FibChannel, FibChannelOutput, FibConfluence, FibConfluenceOutput,
|
||||
FibExtension, FibExtensionOutput, FibFan, FibFanOutput, FibProjection, FibProjectionOutput,
|
||||
FibRetracement, FibRetracementOutput, FibTimeZones, FibTimeZonesOutput, FibonacciPivots,
|
||||
FibonacciPivotsOutput, FisherTransform, FlagPennant, Footprint, FootprintOutput, ForceIndex,
|
||||
FractalChaosBands, FractalChaosBandsOutput, Frama, FundingBasis, FundingRate, FundingRateMean,
|
||||
FundingRateZScore, GainLossRatio, GapSideBySideWhite, GarmanKlassVolatility, Gartley,
|
||||
GoldenPocket, GoldenPocketOutput, GrangerCausality, GravestoneDoji, Hammer, HangingMan, Harami,
|
||||
HeadAndShoulders, HeikinAshi, HeikinAshiOutput, HiLoActivator, HighLowIndex, HighLowRange,
|
||||
HighWave, Hikkake, HikkakeModified, HilbertDominantCycle, HistoricalVolatility, Hma,
|
||||
HomingPigeon, HtDcPhase, HtPhasor, HtPhasorOutput, HtTrendMode, HurstChannel,
|
||||
HurstChannelOutput, HurstExponent, Ichimoku, IchimokuOutput, IdenticalThreeCrows, InNeck,
|
||||
Inertia, InformationRatio, InitialBalance, InitialBalanceOutput, InstantaneousTrendline,
|
||||
DrawdownDuration, Dx, EaseOfMovement, EffectiveSpread, EhlersStochastic, Ehma, ElderImpulse,
|
||||
Ema, EmpiricalModeDecomposition, Engulfing, EveningDojiStar, Evwma, Expectancy,
|
||||
FallingThreeMethods, Fama, FibArcs, FibArcsOutput, FibChannel, FibChannelOutput, FibConfluence,
|
||||
FibConfluenceOutput, FibExtension, FibExtensionOutput, FibFan, FibFanOutput, FibProjection,
|
||||
FibProjectionOutput, FibRetracement, FibRetracementOutput, FibTimeZones, FibTimeZonesOutput,
|
||||
FibonacciPivots, FibonacciPivotsOutput, FisherTransform, FlagPennant, Footprint,
|
||||
FootprintOutput, ForceIndex, FractalChaosBands, FractalChaosBandsOutput, Frama, FundingBasis,
|
||||
FundingRate, FundingRateMean, FundingRateZScore, GainLossRatio, GapSideBySideWhite,
|
||||
GarmanKlassVolatility, Gartley, GeneralizedDema, GeometricMa, GoldenPocket, GoldenPocketOutput,
|
||||
GrangerCausality, GravestoneDoji, Hammer, HangingMan, Harami, HeadAndShoulders, HeikinAshi,
|
||||
HeikinAshiOutput, HiLoActivator, HighLowIndex, HighLowRange, HighWave, Hikkake,
|
||||
HikkakeModified, HilbertDominantCycle, HistoricalVolatility, Hma, HoltWinters, HomingPigeon,
|
||||
HtDcPhase, HtPhasor, HtPhasorOutput, HtTrendMode, HurstChannel, HurstChannelOutput,
|
||||
HurstExponent, Ichimoku, IchimokuOutput, IdenticalThreeCrows, InNeck, Inertia,
|
||||
InformationRatio, InitialBalance, InitialBalanceOutput, InstantaneousTrendline,
|
||||
IntradayVolatilityProfile, IntradayVolatilityProfileOutput, InverseFisherTransform,
|
||||
InvertedHammer, Jma, JumpIndicator, KagiBars, KalmanHedgeRatio, KalmanHedgeRatioOutput, Kama,
|
||||
KellyCriterion, Keltner, KeltnerOutput, Kicking, KickingByLength, Kst, KstOutput, Kurtosis,
|
||||
@@ -97,27 +99,28 @@ pub use indicators::{
|
||||
LogReturn, LongLeggedDoji, LongLine, LongShortRatio, MaEnvelope, MaEnvelopeOutput, MacdExt,
|
||||
MacdFix, MacdIndicator, MacdOutput, Mama, MamaOutput, MarketFacilitationIndex, Marubozu,
|
||||
MassIndex, MatHold, MatchingLow, MaxDrawdown, McClellanOscillator, McClellanSummationIndex,
|
||||
McGinleyDynamic, MedianAbsoluteDeviation, MedianPrice, Mfi, Microprice, MidPoint, MidPrice,
|
||||
MinusDi, MinusDm, Mom, MorningDojiStar, MorningEveningStar, Natr, NewHighsNewLows, Nvi,
|
||||
OIPriceDivergence, OIWeighted, Obv, OmegaRatio, OnNeck, OpenInterestDelta, OpeningMarubozu,
|
||||
OpeningRange, OpeningRangeOutput, OrderBookImbalanceFull, OrderBookImbalanceTop1,
|
||||
OrderBookImbalanceTopN, OrderFlowImbalance, OuHalfLife, OvernightGap, OvernightIntradayReturn,
|
||||
OvernightIntradayReturnOutput, PainIndex, PairSpreadZScore, PairwiseBeta, ParkinsonVolatility,
|
||||
PearsonCorrelation, PercentAboveMa, PercentB, PercentageTrailingStop, Pgo, PiercingDarkCloud,
|
||||
PlusDi, PlusDm, Pmo, PointAndFigureBars, Ppo, ProfitFactor, Psar, Pvi, QuotedSpread, RSquared,
|
||||
RealizedSpread, RealizedVolatility, RecoveryFactor, RectangleRange, RegimeLabel,
|
||||
RelativeStrengthAB, RelativeStrengthOutput, RenkoBars, RenkoTrailingStop, RickshawMan,
|
||||
RisingThreeMethods, Roc, Rocp, Rocr, Rocr100, RogersSatchellVolatility, RollMeasure,
|
||||
RollingCorrelation, RollingCovariance, RollingIqr, RollingPercentileRank, RollingQuantile,
|
||||
RollingVwap, RoofingFilter, Rsi, Rvi, RviVolatility, Rwi, RwiOutput, SarExt, SeasonalZScore,
|
||||
SeparatingLines, SessionHighLow, SessionHighLowOutput, SessionRange, SessionRangeOutput,
|
||||
SessionVwap, Shark, SharpeRatio, ShootingStar, ShortLine, SignedVolume, SineWave, Skewness,
|
||||
Sma, Smi, Smma, SortinoRatio, SpearmanCorrelation, SpinningTop, SpreadAr1Coefficient,
|
||||
SpreadBollingerBands, SpreadBollingerBandsOutput, SpreadHurst, StalledPattern, StandardError,
|
||||
StandardErrorBands, StandardErrorBandsOutput, StarcBands, StarcBandsOutput, Stc, StdDev,
|
||||
StepTrailingStop, StickSandwich, StochRsi, Stochastic, StochasticOutput, SuperSmoother,
|
||||
SuperTrend, SuperTrendOutput, TakerBuySellRatio, Takuri, TasukiGap, TdCombo, TdCountdown,
|
||||
TdDeMarker, TdDifferential, TdLines, TdLinesOutput, TdOpen, TdPressure, TdRangeProjection,
|
||||
McGinleyDynamic, MedianAbsoluteDeviation, MedianMa, MedianPrice, Mfi, Microprice, MidPoint,
|
||||
MidPrice, MinusDi, MinusDm, Mom, MorningDojiStar, MorningEveningStar, Natr, NewHighsNewLows,
|
||||
Nvi, OIPriceDivergence, OIWeighted, Obv, OmegaRatio, OnNeck, OpenInterestDelta,
|
||||
OpeningMarubozu, OpeningRange, OpeningRangeOutput, OrderBookImbalanceFull,
|
||||
OrderBookImbalanceTop1, OrderBookImbalanceTopN, OrderFlowImbalance, OuHalfLife, OvernightGap,
|
||||
OvernightIntradayReturn, OvernightIntradayReturnOutput, PainIndex, PairSpreadZScore,
|
||||
PairwiseBeta, ParkinsonVolatility, PearsonCorrelation, PercentAboveMa, PercentB,
|
||||
PercentageTrailingStop, Pgo, PiercingDarkCloud, PlusDi, PlusDm, Pmo, PointAndFigureBars, Ppo,
|
||||
ProfitFactor, Psar, Pvi, QuotedSpread, RSquared, RealizedSpread, RealizedVolatility,
|
||||
RecoveryFactor, RectangleRange, RegimeLabel, RelativeStrengthAB, RelativeStrengthOutput,
|
||||
RenkoBars, RenkoTrailingStop, RickshawMan, RisingThreeMethods, Roc, Rocp, Rocr, Rocr100,
|
||||
RogersSatchellVolatility, RollMeasure, RollingCorrelation, RollingCovariance, RollingIqr,
|
||||
RollingPercentileRank, RollingQuantile, RollingVwap, RoofingFilter, Rsi, 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, StochasticOutput, SuperSmoother, SuperTrend,
|
||||
SuperTrendOutput, TakerBuySellRatio, Takuri, TasukiGap, TdCombo, TdCountdown, TdDeMarker,
|
||||
TdDifferential, TdLines, TdLinesOutput, TdOpen, TdPressure, TdRangeProjection,
|
||||
TdRangeProjectionOutput, TdRei, TdRiskLevel, TdRiskLevelOutput, TdSequential,
|
||||
TdSequentialOutput, TdSetup, Tema, TermStructureBasis, ThreeDrives, ThreeInside,
|
||||
ThreeLineStrike, ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth, Thrusting, TickIndex,
|
||||
|
||||
+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 **396 indicators** across
|
||||
- A per-indicator deep dive for every one of the **403 indicators** across
|
||||
the sixteen families (Moving Averages, Momentum Oscillators, Trend &
|
||||
Directional, Price Oscillators, Volatility & Bands, Bands & Channels,
|
||||
Trailing Stops, Volume, Price Statistics, Ehlers / Cycle DSP, Pivots &
|
||||
|
||||
Generated
+7
-7
@@ -17,7 +17,7 @@
|
||||
},
|
||||
"../../bindings/node": {
|
||||
"name": "wickra",
|
||||
"version": "0.5.4",
|
||||
"version": "0.5.5",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"devDependencies": {
|
||||
"@napi-rs/cli": "^2.18.0"
|
||||
@@ -26,12 +26,12 @@
|
||||
"node": ">= 18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"wickra-darwin-arm64": "0.5.4",
|
||||
"wickra-darwin-x64": "0.5.4",
|
||||
"wickra-linux-arm64-gnu": "0.5.4",
|
||||
"wickra-linux-x64-gnu": "0.5.4",
|
||||
"wickra-win32-arm64-msvc": "0.5.4",
|
||||
"wickra-win32-x64-msvc": "0.5.4"
|
||||
"wickra-darwin-arm64": "0.5.5",
|
||||
"wickra-darwin-x64": "0.5.5",
|
||||
"wickra-linux-arm64-gnu": "0.5.5",
|
||||
"wickra-linux-x64-gnu": "0.5.5",
|
||||
"wickra-win32-arm64-msvc": "0.5.5",
|
||||
"wickra-win32-x64-msvc": "0.5.5"
|
||||
}
|
||||
},
|
||||
"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, Alma, AnchoredRsi, Apo, Autocorrelation, AverageDrawdown, BatchExt, Beta, BollingerBands, CalmarRatio, CenterOfGravity, Cfo, Cmo, CoefficientOfVariation, ConditionalValueAtRisk, ConnorsRsi, Coppock, CyberneticCycle, Decycler, DecyclerOscillator, Dema, DetrendedStdDev, DoubleBollinger, Dpo, DrawdownDuration, EhlersStochastic, ElderImpulse, Ema, EmpiricalModeDecomposition, Expectancy, Fama, FisherTransform, Frama, GainLossRatio, HilbertDominantCycle, HistoricalVolatility, Hma, HtDcPhase, HtPhasor, HtTrendMode, HurstExponent, Indicator, InstantaneousTrendline, InverseFisherTransform, Jma, JumpIndicator, Kama, KellyCriterion, Kst, Kurtosis, LaguerreRsi, LinRegAngle, LinRegChannel, LinRegIntercept, LinRegSlope, LinearRegression, LogReturn, MaEnvelope, MaType, MacdExt, MacdFix, MacdIndicator, Mama, MaxDrawdown, McGinleyDynamic, MedianAbsoluteDeviation, MidPoint, Mom, OmegaRatio, PainIndex, PearsonCorrelation, PercentageTrailingStop, Pmo, Ppo, ProfitFactor, RSquared, RealizedVolatility, RecoveryFactor, RegimeLabel, RenkoTrailingStop, Roc, Rocp, Rocr, Rocr100, RollingIqr, RollingPercentileRank, RollingQuantile, RoofingFilter, Rsi, RviVolatility, SharpeRatio, SineWave, Skewness, Sma, Smma, SortinoRatio, SpearmanCorrelation, StandardError, StandardErrorBands, Stc, StdDev, StepTrailingStop, StochRsi, SuperSmoother, Tema, Tii, TrendLabel, Trima, Trix, Tsf, Tsi, UlcerIndex, ValueAtRisk, Variance, VerticalHorizontalFilter, Vidya, WinRate, Wma, ZScore, ZeroLagMacd, Zlema, T3};
|
||||
use wickra_core::{AdaptiveCycle, AdaptiveLaguerreFilter, Alma, AnchoredRsi, Apo, Autocorrelation, AverageDrawdown, BatchExt, Beta, BollingerBands, CalmarRatio, CenterOfGravity, Cfo, Cmo, CoefficientOfVariation, ConditionalValueAtRisk, ConnorsRsi, Coppock, CyberneticCycle, Decycler, DecyclerOscillator, Dema, DetrendedStdDev, DoubleBollinger, Dpo, DrawdownDuration, EhlersStochastic, Ehma, ElderImpulse, Ema, EmpiricalModeDecomposition, Expectancy, Fama, FisherTransform, Frama, GainLossRatio, GeneralizedDema, GeometricMa, HilbertDominantCycle, HistoricalVolatility, Hma, HoltWinters, HtDcPhase, HtPhasor, HtTrendMode, HurstExponent, Indicator, InstantaneousTrendline, InverseFisherTransform, Jma, JumpIndicator, Kama, KellyCriterion, Kst, Kurtosis, LaguerreRsi, LinRegAngle, LinRegChannel, LinRegIntercept, LinRegSlope, LinearRegression, LogReturn, MaEnvelope, MaType, MacdExt, MacdFix, MacdIndicator, Mama, MaxDrawdown, McGinleyDynamic, MedianAbsoluteDeviation, MedianMa, MidPoint, Mom, OmegaRatio, PainIndex, PearsonCorrelation, PercentageTrailingStop, Pmo, Ppo, ProfitFactor, RSquared, RealizedVolatility, RecoveryFactor, RegimeLabel, RenkoTrailingStop, Roc, Rocp, Rocr, Rocr100, RollingIqr, RollingPercentileRank, RollingQuantile, RoofingFilter, Rsi, RviVolatility, SharpeRatio, SineWave, SineWeightedMa, Skewness, Sma, Smma, SortinoRatio, SpearmanCorrelation, StandardError, StandardErrorBands, Stc, StdDev, StepTrailingStop, StochRsi, SuperSmoother, Tema, Tii, TrendLabel, Trima, Trix, Tsf, Tsi, UlcerIndex, ValueAtRisk, Variance, VerticalHorizontalFilter, Vidya, WinRate, Wma, ZScore, ZeroLagMacd, Zlema, T3};
|
||||
|
||||
/// Drive a single streaming + batch run through one scalar indicator. Marked
|
||||
/// `#[inline(never)]` so a panic backtrace pin-points the specific indicator.
|
||||
@@ -43,6 +43,13 @@ fuzz_target!(|data: Vec<f64>| {
|
||||
drive(|| Dema::new(14).unwrap(), &data);
|
||||
drive(|| Tema::new(14).unwrap(), &data);
|
||||
drive(|| Hma::new(14).unwrap(), &data);
|
||||
drive(|| SineWeightedMa::new(14).unwrap(), &data);
|
||||
drive(|| GeometricMa::new(14).unwrap(), &data);
|
||||
drive(|| Ehma::new(9).unwrap(), &data);
|
||||
drive(|| MedianMa::new(14).unwrap(), &data);
|
||||
drive(|| AdaptiveLaguerreFilter::new(13).unwrap(), &data);
|
||||
drive(|| GeneralizedDema::new(5, 0.7).unwrap(), &data);
|
||||
drive(|| HoltWinters::new(0.2, 0.1).unwrap(), &data);
|
||||
drive(|| Roc::new(14).unwrap(), &data);
|
||||
drive(|| Rocp::new(14).unwrap(), &data);
|
||||
drive(|| Rocr::new(14).unwrap(), &data);
|
||||
|
||||
Reference in New Issue
Block a user