Compare 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]
|
## [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
|
## [0.5.4] - 2026-06-04
|
||||||
- **Roll Measure** — effective spread implied by the negative serial covariance of trade-price changes (Roll 1984) (`RollMeasure`).
|
- **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`).
|
- **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.
|
optional Binance live feed.
|
||||||
- Bindings for Python, Node.js, and WebAssembly.
|
- 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.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.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
|
[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]]
|
[[package]]
|
||||||
name = "wickra"
|
name = "wickra"
|
||||||
version = "0.5.4"
|
version = "0.5.5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"approx",
|
"approx",
|
||||||
"criterion",
|
"criterion",
|
||||||
@@ -1878,7 +1878,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wickra-core"
|
name = "wickra-core"
|
||||||
version = "0.5.4"
|
version = "0.5.5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"approx",
|
"approx",
|
||||||
"proptest",
|
"proptest",
|
||||||
@@ -1888,7 +1888,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wickra-data"
|
name = "wickra-data"
|
||||||
version = "0.5.4"
|
version = "0.5.5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"approx",
|
"approx",
|
||||||
"csv",
|
"csv",
|
||||||
@@ -1915,7 +1915,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wickra-node"
|
name = "wickra-node"
|
||||||
version = "0.5.4"
|
version = "0.5.5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"napi",
|
"napi",
|
||||||
"napi-build",
|
"napi-build",
|
||||||
@@ -1925,7 +1925,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wickra-python"
|
name = "wickra-python"
|
||||||
version = "0.5.4"
|
version = "0.5.5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"numpy",
|
"numpy",
|
||||||
"pyo3",
|
"pyo3",
|
||||||
@@ -1934,7 +1934,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wickra-wasm"
|
name = "wickra-wasm"
|
||||||
version = "0.5.4"
|
version = "0.5.5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"console_error_panic_hook",
|
"console_error_panic_hook",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
|
|||||||
+2
-2
@@ -12,7 +12,7 @@ members = [
|
|||||||
exclude = ["fuzz"]
|
exclude = ["fuzz"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.5.4"
|
version = "0.5.5"
|
||||||
authors = ["kingchenc <support@wickra.org>"]
|
authors = ["kingchenc <support@wickra.org>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.86"
|
rust-version = "1.86"
|
||||||
@@ -24,7 +24,7 @@ keywords = ["finance", "trading", "indicators", "technical-analysis", "ta"]
|
|||||||
categories = ["finance", "mathematics", "science"]
|
categories = ["finance", "mathematics", "science"]
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
wickra-core = { path = "crates/wickra-core", version = "0.5.4" }
|
wickra-core = { path = "crates/wickra-core", version = "0.5.5" }
|
||||||
|
|
||||||
thiserror = "2"
|
thiserror = "2"
|
||||||
rayon = "1.10"
|
rayon = "1.10"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<p align="center">
|
<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>
|
</p>
|
||||||
|
|
||||||
[](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml)
|
[](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),
|
[Node](https://docs.wickra.org/Quickstart-Node),
|
||||||
[WASM](https://docs.wickra.org/Quickstart-WASM).
|
[WASM](https://docs.wickra.org/Quickstart-WASM).
|
||||||
- **Indicators** — a per-indicator deep dive (formula, parameters, warmup) for
|
- **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).
|
[indicators overview](https://docs.wickra.org/Indicators-Overview).
|
||||||
- **Reference** — [warmup periods](https://docs.wickra.org/Warmup-Periods),
|
- **Reference** — [warmup periods](https://docs.wickra.org/Warmup-Periods),
|
||||||
[streaming vs batch](https://docs.wickra.org/Streaming-vs-Batch),
|
[streaming vs batch](https://docs.wickra.org/Streaming-vs-Batch),
|
||||||
@@ -136,14 +136,14 @@ python -m benchmarks.compare_libraries
|
|||||||
|
|
||||||
## Indicators
|
## 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
|
`batch == streaming` equivalence test, reference-value tests, and reset
|
||||||
semantics tests. Each has a per-indicator deep dive (formula, parameters,
|
semantics tests. Each has a per-indicator deep dive (formula, parameters,
|
||||||
warmup) at [docs.wickra.org](https://docs.wickra.org/Indicators-Overview).
|
warmup) at [docs.wickra.org](https://docs.wickra.org/Indicators-Overview).
|
||||||
|
|
||||||
| Family | Indicators |
|
| 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) |
|
| 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 |
|
| 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 |
|
| 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/
|
wickra/
|
||||||
├── crates/
|
├── 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/ top-level facade crate (publishes on crates.io) + benches/
|
||||||
│ └── wickra-data/ CSV reader, tick aggregator, live exchange feeds
|
│ └── wickra-data/ CSV reader, tick aggregator, live exchange feeds
|
||||||
├── bindings/
|
├── bindings/
|
||||||
|
|||||||
@@ -28,6 +28,13 @@ function num(v) {
|
|||||||
// --- Scalar indicators: update(value) vs batch(prices) ---
|
// --- Scalar indicators: update(value) vs batch(prices) ---
|
||||||
|
|
||||||
const scalarFactories = {
|
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),
|
Expectancy: () => new wickra.Expectancy(20),
|
||||||
WinRate: () => new wickra.WinRate(20),
|
WinRate: () => new wickra.WinRate(20),
|
||||||
RegimeLabel: () => new wickra.RegimeLabel(5, 20),
|
RegimeLabel: () => new wickra.RegimeLabel(5, 20),
|
||||||
|
|||||||
Vendored
+63
@@ -872,6 +872,51 @@ export declare class Expectancy {
|
|||||||
isReady(): boolean
|
isReady(): boolean
|
||||||
warmupPeriod(): number
|
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 type JumpIndicatorNode = JumpIndicator
|
||||||
export declare class JumpIndicator {
|
export declare class JumpIndicator {
|
||||||
constructor(period: number, threshold: number)
|
constructor(period: number, threshold: number)
|
||||||
@@ -1677,6 +1722,24 @@ export declare class T3 {
|
|||||||
isReady(): boolean
|
isReady(): boolean
|
||||||
warmupPeriod(): number
|
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 type TsiNode = TSI
|
||||||
export declare class TSI {
|
export declare class TSI {
|
||||||
constructor(long: number, short: number)
|
constructor(long: number, short: number)
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "wickra-darwin-arm64",
|
"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.",
|
"description": "Native binding for wickra (macOS Apple Silicon). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||||
"main": "wickra.darwin-arm64.node",
|
"main": "wickra.darwin-arm64.node",
|
||||||
"files": [
|
"files": [
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "wickra-darwin-x64",
|
"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.",
|
"description": "Native binding for wickra (macOS Intel). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||||
"main": "wickra.darwin-x64.node",
|
"main": "wickra.darwin-x64.node",
|
||||||
"files": [
|
"files": [
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "wickra-linux-arm64-gnu",
|
"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.",
|
"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",
|
"main": "wickra.linux-arm64-gnu.node",
|
||||||
"files": [
|
"files": [
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "wickra-linux-x64-gnu",
|
"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.",
|
"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",
|
"main": "wickra.linux-x64-gnu.node",
|
||||||
"files": [
|
"files": [
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "wickra-win32-arm64-msvc",
|
"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.",
|
"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",
|
"main": "wickra.win32-arm64-msvc.node",
|
||||||
"files": [
|
"files": [
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "wickra-win32-x64-msvc",
|
"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.",
|
"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",
|
"main": "wickra.win32-x64-msvc.node",
|
||||||
"files": [
|
"files": [
|
||||||
|
|||||||
Generated
+20
-20
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "wickra",
|
"name": "wickra",
|
||||||
"version": "0.5.4",
|
"version": "0.5.5",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "wickra",
|
"name": "wickra",
|
||||||
"version": "0.5.4",
|
"version": "0.5.5",
|
||||||
"license": "MIT OR Apache-2.0",
|
"license": "MIT OR Apache-2.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@napi-rs/cli": "^2.18.0"
|
"@napi-rs/cli": "^2.18.0"
|
||||||
@@ -15,12 +15,12 @@
|
|||||||
"node": ">= 18"
|
"node": ">= 18"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"wickra-darwin-arm64": "0.5.4",
|
"wickra-darwin-arm64": "0.5.5",
|
||||||
"wickra-darwin-x64": "0.5.4",
|
"wickra-darwin-x64": "0.5.5",
|
||||||
"wickra-linux-arm64-gnu": "0.5.4",
|
"wickra-linux-arm64-gnu": "0.5.5",
|
||||||
"wickra-linux-x64-gnu": "0.5.4",
|
"wickra-linux-x64-gnu": "0.5.5",
|
||||||
"wickra-win32-arm64-msvc": "0.5.4",
|
"wickra-win32-arm64-msvc": "0.5.5",
|
||||||
"wickra-win32-x64-msvc": "0.5.4"
|
"wickra-win32-x64-msvc": "0.5.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@napi-rs/cli": {
|
"node_modules/@napi-rs/cli": {
|
||||||
@@ -41,8 +41,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/wickra-darwin-arm64": {
|
"node_modules/wickra-darwin-arm64": {
|
||||||
"version": "0.5.4",
|
"version": "0.5.5",
|
||||||
"resolved": "https://registry.npmjs.org/wickra-darwin-arm64/-/wickra-darwin-arm64-0.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/wickra-darwin-arm64/-/wickra-darwin-arm64-0.5.5.tgz",
|
||||||
"integrity": "sha512-4eZiBR/yGUdr4nzhEUFy2i69XgNx64iI2ax/LPamsThgylC0KpHOZKK19QzJ2d9KbK4C8nMjME5FLuR+4GNEwQ==",
|
"integrity": "sha512-4eZiBR/yGUdr4nzhEUFy2i69XgNx64iI2ax/LPamsThgylC0KpHOZKK19QzJ2d9KbK4C8nMjME5FLuR+4GNEwQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
@@ -57,8 +57,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/wickra-darwin-x64": {
|
"node_modules/wickra-darwin-x64": {
|
||||||
"version": "0.5.4",
|
"version": "0.5.5",
|
||||||
"resolved": "https://registry.npmjs.org/wickra-darwin-x64/-/wickra-darwin-x64-0.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/wickra-darwin-x64/-/wickra-darwin-x64-0.5.5.tgz",
|
||||||
"integrity": "sha512-6hf8zI3QPjTFp4zCpmgUwDvNtu6jHqNUHKD5e55POo0CgA52HkpyxSPtVm8TGTIZDI7kPjlbOdBM8CJ76mmXwA==",
|
"integrity": "sha512-6hf8zI3QPjTFp4zCpmgUwDvNtu6jHqNUHKD5e55POo0CgA52HkpyxSPtVm8TGTIZDI7kPjlbOdBM8CJ76mmXwA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
@@ -73,8 +73,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/wickra-linux-arm64-gnu": {
|
"node_modules/wickra-linux-arm64-gnu": {
|
||||||
"version": "0.5.4",
|
"version": "0.5.5",
|
||||||
"resolved": "https://registry.npmjs.org/wickra-linux-arm64-gnu/-/wickra-linux-arm64-gnu-0.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/wickra-linux-arm64-gnu/-/wickra-linux-arm64-gnu-0.5.5.tgz",
|
||||||
"integrity": "sha512-kSe6y0xBMSiqdPLXNjwop5WZdHtvdBNKSEBCwZ4hFq33p4apW25/wrlzv9/oDuyD4kuPabJEhCCnFOplh58CUg==",
|
"integrity": "sha512-kSe6y0xBMSiqdPLXNjwop5WZdHtvdBNKSEBCwZ4hFq33p4apW25/wrlzv9/oDuyD4kuPabJEhCCnFOplh58CUg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
@@ -89,8 +89,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/wickra-linux-x64-gnu": {
|
"node_modules/wickra-linux-x64-gnu": {
|
||||||
"version": "0.5.4",
|
"version": "0.5.5",
|
||||||
"resolved": "https://registry.npmjs.org/wickra-linux-x64-gnu/-/wickra-linux-x64-gnu-0.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/wickra-linux-x64-gnu/-/wickra-linux-x64-gnu-0.5.5.tgz",
|
||||||
"integrity": "sha512-tWBWS4qz7hxM4xnpFb59bhf6TaLwXq0Z3jEa/2l7r8PiHA94g8r8S53NRMiT+4yiL5hSWe/nUiC/YXdRrhEZ4g==",
|
"integrity": "sha512-tWBWS4qz7hxM4xnpFb59bhf6TaLwXq0Z3jEa/2l7r8PiHA94g8r8S53NRMiT+4yiL5hSWe/nUiC/YXdRrhEZ4g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
@@ -105,8 +105,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/wickra-win32-arm64-msvc": {
|
"node_modules/wickra-win32-arm64-msvc": {
|
||||||
"version": "0.5.4",
|
"version": "0.5.5",
|
||||||
"resolved": "https://registry.npmjs.org/wickra-win32-arm64-msvc/-/wickra-win32-arm64-msvc-0.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/wickra-win32-arm64-msvc/-/wickra-win32-arm64-msvc-0.5.5.tgz",
|
||||||
"integrity": "sha512-EXIckHxAtF75PUGDKRzXyqMe9ldP0JjSdu68WFN6iJfp+McYrGu6h40TEJlQ/oUEIoPqiZB/xhVyo/el5Lg7zw==",
|
"integrity": "sha512-EXIckHxAtF75PUGDKRzXyqMe9ldP0JjSdu68WFN6iJfp+McYrGu6h40TEJlQ/oUEIoPqiZB/xhVyo/el5Lg7zw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
@@ -121,8 +121,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/wickra-win32-x64-msvc": {
|
"node_modules/wickra-win32-x64-msvc": {
|
||||||
"version": "0.5.4",
|
"version": "0.5.5",
|
||||||
"resolved": "https://registry.npmjs.org/wickra-win32-x64-msvc/-/wickra-win32-x64-msvc-0.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/wickra-win32-x64-msvc/-/wickra-win32-x64-msvc-0.5.5.tgz",
|
||||||
"integrity": "sha512-Yfsqq1Xwp6hdxMyLze411vNdo7BDwI6+lPSe7A9XdqyPecNDbtKwYLpsal2r8EHbNzqM+R8XnuRtUaEQS5VlUQ==",
|
"integrity": "sha512-Yfsqq1Xwp6hdxMyLze411vNdo7BDwI6+lPSe7A9XdqyPecNDbtKwYLpsal2r8EHbNzqM+R8XnuRtUaEQS5VlUQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "wickra",
|
"name": "wickra",
|
||||||
"version": "0.5.4",
|
"version": "0.5.5",
|
||||||
"description": "Streaming-first technical indicators: incremental, fast, install-free. Node bindings powered by Rust.",
|
"description": "Streaming-first technical indicators: incremental, fast, install-free. Node bindings powered by Rust.",
|
||||||
"author": "kingchenc <support@wickra.org>",
|
"author": "kingchenc <support@wickra.org>",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
@@ -47,12 +47,12 @@
|
|||||||
"node": ">= 18"
|
"node": ">= 18"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"wickra-linux-x64-gnu": "0.5.4",
|
"wickra-linux-x64-gnu": "0.5.5",
|
||||||
"wickra-linux-arm64-gnu": "0.5.4",
|
"wickra-linux-arm64-gnu": "0.5.5",
|
||||||
"wickra-darwin-x64": "0.5.4",
|
"wickra-darwin-x64": "0.5.5",
|
||||||
"wickra-darwin-arm64": "0.5.4",
|
"wickra-darwin-arm64": "0.5.5",
|
||||||
"wickra-win32-x64-msvc": "0.5.4",
|
"wickra-win32-x64-msvc": "0.5.5",
|
||||||
"wickra-win32-arm64-msvc": "0.5.4"
|
"wickra-win32-arm64-msvc": "0.5.5"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "napi build --platform --release",
|
"build": "napi build --platform --release",
|
||||||
|
|||||||
@@ -197,6 +197,15 @@ node_scalar_indicator!(
|
|||||||
node_scalar_indicator!(TrendLabelNode, "TrendLabel", wc::TrendLabel);
|
node_scalar_indicator!(TrendLabelNode, "TrendLabel", wc::TrendLabel);
|
||||||
node_scalar_indicator!(WinRateNode, "WinRate", wc::WinRate);
|
node_scalar_indicator!(WinRateNode, "WinRate", wc::WinRate);
|
||||||
node_scalar_indicator!(ExpectancyNode, "Expectancy", wc::Expectancy);
|
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")]
|
#[napi(js_name = "JumpIndicator")]
|
||||||
pub struct JumpIndicatorNode {
|
pub struct JumpIndicatorNode {
|
||||||
inner: wc::JumpIndicator,
|
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 ==============================
|
// ============================== TSI ==============================
|
||||||
|
|
||||||
#[napi(js_name = "TSI")]
|
#[napi(js_name = "TSI")]
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "maturin"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "wickra"
|
name = "wickra"
|
||||||
version = "0.5.4"
|
version = "0.5.5"
|
||||||
description = "Streaming-first technical indicators: incremental, fast, install-free."
|
description = "Streaming-first technical indicators: incremental, fast, install-free."
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
|
|||||||
@@ -25,6 +25,13 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from ._wickra import (
|
from ._wickra import (
|
||||||
__version__,
|
__version__,
|
||||||
|
HoltWinters,
|
||||||
|
GD,
|
||||||
|
AdaptiveLaguerre,
|
||||||
|
MedianMA,
|
||||||
|
EHMA,
|
||||||
|
GMA,
|
||||||
|
SWMA,
|
||||||
Expectancy,
|
Expectancy,
|
||||||
WinRate,
|
WinRate,
|
||||||
RegimeLabel,
|
RegimeLabel,
|
||||||
@@ -449,6 +456,13 @@ from ._wickra import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
"HoltWinters",
|
||||||
|
"GD",
|
||||||
|
"AdaptiveLaguerre",
|
||||||
|
"MedianMA",
|
||||||
|
"EHMA",
|
||||||
|
"GMA",
|
||||||
|
"SWMA",
|
||||||
"Expectancy",
|
"Expectancy",
|
||||||
"WinRate",
|
"WinRate",
|
||||||
"RegimeLabel",
|
"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 ==============================
|
// ============================== Stochastic ==============================
|
||||||
|
|
||||||
#[pyclass(name = "Stochastic", module = "wickra._wickra", skip_from_py_object)]
|
#[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 ==============================
|
// ============================== VWMA ==============================
|
||||||
|
|
||||||
#[pyclass(name = "VWMA", module = "wickra._wickra", skip_from_py_object)]
|
#[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::<PyTrima>()?;
|
||||||
m.add_class::<PyZlema>()?;
|
m.add_class::<PyZlema>()?;
|
||||||
m.add_class::<PyT3>()?;
|
m.add_class::<PyT3>()?;
|
||||||
|
m.add_class::<PyGeneralizedDema>()?;
|
||||||
|
m.add_class::<PyHoltWinters>()?;
|
||||||
m.add_class::<PyVwma>()?;
|
m.add_class::<PyVwma>()?;
|
||||||
m.add_class::<PyMom>()?;
|
m.add_class::<PyMom>()?;
|
||||||
m.add_class::<PyCmo>()?;
|
m.add_class::<PyCmo>()?;
|
||||||
@@ -20031,5 +20401,10 @@ fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
|
|||||||
m.add_class::<PyRegimeLabel>()?;
|
m.add_class::<PyRegimeLabel>()?;
|
||||||
m.add_class::<PyWinRate>()?;
|
m.add_class::<PyWinRate>()?;
|
||||||
m.add_class::<PyExpectancy>()?;
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,13 @@ def ohlcv() -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
|
|||||||
# --- Scalar (f64 -> f64) indicators ---------------------------------------
|
# --- Scalar (f64 -> f64) indicators ---------------------------------------
|
||||||
|
|
||||||
SCALAR = [
|
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.Expectancy, (20,)),
|
||||||
(ta.WinRate, (20,)),
|
(ta.WinRate, (20,)),
|
||||||
(ta.RegimeLabel, (5, 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!(WasmRegimeLabel, "RegimeLabel", wc::RegimeLabel, vol_period: usize, lookback: usize);
|
||||||
wasm_scalar_indicator!(WasmWinRate, "WinRate", wc::WinRate, period: usize);
|
wasm_scalar_indicator!(WasmWinRate, "WinRate", wc::WinRate, period: usize);
|
||||||
wasm_scalar_indicator!(WasmExpectancy, "Expectancy", wc::Expectancy, 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 ---
|
// --- 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_oscillator;
|
||||||
mod ad_volume_line;
|
mod ad_volume_line;
|
||||||
mod adaptive_cycle;
|
mod adaptive_cycle;
|
||||||
|
mod adaptive_laguerre_filter;
|
||||||
mod adl;
|
mod adl;
|
||||||
mod advance_block;
|
mod advance_block;
|
||||||
mod advance_decline;
|
mod advance_decline;
|
||||||
@@ -106,6 +107,7 @@ mod dx;
|
|||||||
mod ease_of_movement;
|
mod ease_of_movement;
|
||||||
mod effective_spread;
|
mod effective_spread;
|
||||||
mod ehlers_stochastic;
|
mod ehlers_stochastic;
|
||||||
|
mod ehma;
|
||||||
mod elder_impulse;
|
mod elder_impulse;
|
||||||
mod ema;
|
mod ema;
|
||||||
mod empirical_mode_decomposition;
|
mod empirical_mode_decomposition;
|
||||||
@@ -138,6 +140,8 @@ mod gain_loss_ratio;
|
|||||||
mod gap_side_by_side_white;
|
mod gap_side_by_side_white;
|
||||||
mod garman_klass;
|
mod garman_klass;
|
||||||
mod gartley;
|
mod gartley;
|
||||||
|
mod generalized_dema;
|
||||||
|
mod geometric_ma;
|
||||||
mod golden_pocket;
|
mod golden_pocket;
|
||||||
mod granger_causality;
|
mod granger_causality;
|
||||||
mod gravestone_doji;
|
mod gravestone_doji;
|
||||||
@@ -155,6 +159,7 @@ mod hilbert_dominant_cycle;
|
|||||||
mod hilo_activator;
|
mod hilo_activator;
|
||||||
mod historical_volatility;
|
mod historical_volatility;
|
||||||
mod hma;
|
mod hma;
|
||||||
|
mod holt_winters;
|
||||||
mod homing_pigeon;
|
mod homing_pigeon;
|
||||||
mod ht_dcphase;
|
mod ht_dcphase;
|
||||||
mod ht_phasor;
|
mod ht_phasor;
|
||||||
@@ -212,6 +217,7 @@ mod mcclellan_oscillator;
|
|||||||
mod mcclellan_summation_index;
|
mod mcclellan_summation_index;
|
||||||
mod mcginley_dynamic;
|
mod mcginley_dynamic;
|
||||||
mod median_absolute_deviation;
|
mod median_absolute_deviation;
|
||||||
|
mod median_ma;
|
||||||
mod median_price;
|
mod median_price;
|
||||||
mod mfi;
|
mod mfi;
|
||||||
mod microprice;
|
mod microprice;
|
||||||
@@ -298,6 +304,7 @@ mod shooting_star;
|
|||||||
mod short_line;
|
mod short_line;
|
||||||
mod signed_volume;
|
mod signed_volume;
|
||||||
mod sine_wave;
|
mod sine_wave;
|
||||||
|
mod sine_weighted_ma;
|
||||||
mod skewness;
|
mod skewness;
|
||||||
mod sma;
|
mod sma;
|
||||||
mod smi;
|
mod smi;
|
||||||
@@ -413,6 +420,7 @@ pub use accelerator_oscillator::AcceleratorOscillator;
|
|||||||
pub use ad_oscillator::AdOscillator;
|
pub use ad_oscillator::AdOscillator;
|
||||||
pub use ad_volume_line::AdVolumeLine;
|
pub use ad_volume_line::AdVolumeLine;
|
||||||
pub use adaptive_cycle::AdaptiveCycle;
|
pub use adaptive_cycle::AdaptiveCycle;
|
||||||
|
pub use adaptive_laguerre_filter::AdaptiveLaguerreFilter;
|
||||||
pub use adl::Adl;
|
pub use adl::Adl;
|
||||||
pub use advance_block::AdvanceBlock;
|
pub use advance_block::AdvanceBlock;
|
||||||
pub use advance_decline::AdvanceDecline;
|
pub use advance_decline::AdvanceDecline;
|
||||||
@@ -502,6 +510,7 @@ pub use dx::Dx;
|
|||||||
pub use ease_of_movement::EaseOfMovement;
|
pub use ease_of_movement::EaseOfMovement;
|
||||||
pub use effective_spread::EffectiveSpread;
|
pub use effective_spread::EffectiveSpread;
|
||||||
pub use ehlers_stochastic::EhlersStochastic;
|
pub use ehlers_stochastic::EhlersStochastic;
|
||||||
|
pub use ehma::Ehma;
|
||||||
pub use elder_impulse::ElderImpulse;
|
pub use elder_impulse::ElderImpulse;
|
||||||
pub use ema::Ema;
|
pub use ema::Ema;
|
||||||
pub use empirical_mode_decomposition::EmpiricalModeDecomposition;
|
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 gap_side_by_side_white::GapSideBySideWhite;
|
||||||
pub use garman_klass::GarmanKlassVolatility;
|
pub use garman_klass::GarmanKlassVolatility;
|
||||||
pub use gartley::Gartley;
|
pub use gartley::Gartley;
|
||||||
|
pub use generalized_dema::GeneralizedDema;
|
||||||
|
pub use geometric_ma::GeometricMa;
|
||||||
pub use golden_pocket::{GoldenPocket, GoldenPocketOutput};
|
pub use golden_pocket::{GoldenPocket, GoldenPocketOutput};
|
||||||
pub use granger_causality::GrangerCausality;
|
pub use granger_causality::GrangerCausality;
|
||||||
pub use gravestone_doji::GravestoneDoji;
|
pub use gravestone_doji::GravestoneDoji;
|
||||||
@@ -551,6 +562,7 @@ pub use hilbert_dominant_cycle::HilbertDominantCycle;
|
|||||||
pub use hilo_activator::HiLoActivator;
|
pub use hilo_activator::HiLoActivator;
|
||||||
pub use historical_volatility::HistoricalVolatility;
|
pub use historical_volatility::HistoricalVolatility;
|
||||||
pub use hma::Hma;
|
pub use hma::Hma;
|
||||||
|
pub use holt_winters::HoltWinters;
|
||||||
pub use homing_pigeon::HomingPigeon;
|
pub use homing_pigeon::HomingPigeon;
|
||||||
pub use ht_dcphase::HtDcPhase;
|
pub use ht_dcphase::HtDcPhase;
|
||||||
pub use ht_phasor::{HtPhasor, HtPhasorOutput};
|
pub use ht_phasor::{HtPhasor, HtPhasorOutput};
|
||||||
@@ -608,6 +620,7 @@ pub use mcclellan_oscillator::McClellanOscillator;
|
|||||||
pub use mcclellan_summation_index::McClellanSummationIndex;
|
pub use mcclellan_summation_index::McClellanSummationIndex;
|
||||||
pub use mcginley_dynamic::McGinleyDynamic;
|
pub use mcginley_dynamic::McGinleyDynamic;
|
||||||
pub use median_absolute_deviation::MedianAbsoluteDeviation;
|
pub use median_absolute_deviation::MedianAbsoluteDeviation;
|
||||||
|
pub use median_ma::MedianMa;
|
||||||
pub use median_price::MedianPrice;
|
pub use median_price::MedianPrice;
|
||||||
pub use mfi::Mfi;
|
pub use mfi::Mfi;
|
||||||
pub use microprice::Microprice;
|
pub use microprice::Microprice;
|
||||||
@@ -694,6 +707,7 @@ pub use shooting_star::ShootingStar;
|
|||||||
pub use short_line::ShortLine;
|
pub use short_line::ShortLine;
|
||||||
pub use signed_volume::SignedVolume;
|
pub use signed_volume::SignedVolume;
|
||||||
pub use sine_wave::SineWave;
|
pub use sine_wave::SineWave;
|
||||||
|
pub use sine_weighted_ma::SineWeightedMa;
|
||||||
pub use skewness::Skewness;
|
pub use skewness::Skewness;
|
||||||
pub use sma::Sma;
|
pub use sma::Sma;
|
||||||
pub use smi::Smi;
|
pub use smi::Smi;
|
||||||
@@ -830,6 +844,13 @@ pub const FAMILIES: &[(&str, &[&str])] = &[
|
|||||||
"Jma",
|
"Jma",
|
||||||
"Alligator",
|
"Alligator",
|
||||||
"Evwma",
|
"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
|
// the actual indicator count is the early-warning signal that an
|
||||||
// indicator was added without being assigned a family.
|
// indicator was added without being assigned a family.
|
||||||
let total: usize = FAMILIES.iter().map(|(_, ns)| ns.len()).sum();
|
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 error::{Error, Result};
|
||||||
pub use indicators::{
|
pub use indicators::{
|
||||||
AbandonedBaby, Abcd, AbsoluteBreadthIndex, AccelerationBands, AccelerationBandsOutput,
|
AbandonedBaby, Abcd, AbsoluteBreadthIndex, AccelerationBands, AccelerationBandsOutput,
|
||||||
AcceleratorOscillator, AdOscillator, AdVolumeLine, AdaptiveCycle, Adl, AdvanceBlock,
|
AcceleratorOscillator, AdOscillator, AdVolumeLine, AdaptiveCycle, AdaptiveLaguerreFilter, Adl,
|
||||||
AdvanceDecline, AdvanceDeclineRatio, Adx, AdxOutput, Adxr, Alligator, AlligatorOutput, Alma,
|
AdvanceBlock, AdvanceDecline, AdvanceDeclineRatio, Adx, AdxOutput, Adxr, Alligator,
|
||||||
Alpha, AmihudIlliquidity, AnchoredRsi, AnchoredVwap, Apo, Aroon, AroonOscillator, AroonOutput,
|
AlligatorOutput, Alma, Alpha, AmihudIlliquidity, AnchoredRsi, AnchoredVwap, Apo, Aroon,
|
||||||
Atr, AtrBands, AtrBandsOutput, AtrTrailingStop, AutoFib, AutoFibOutput, Autocorrelation,
|
AroonOscillator, AroonOutput, Atr, AtrBands, AtrBandsOutput, AtrTrailingStop, AutoFib,
|
||||||
AverageDailyRange, AverageDrawdown, AvgPrice, AwesomeOscillator, AwesomeOscillatorHistogram,
|
AutoFibOutput, Autocorrelation, AverageDailyRange, AverageDrawdown, AvgPrice,
|
||||||
BalanceOfPower, Bat, BeltHold, Beta, BetaNeutralSpread, BodySizePct, BollingerBands,
|
AwesomeOscillator, AwesomeOscillatorHistogram, BalanceOfPower, Bat, BeltHold, Beta,
|
||||||
BollingerBandwidth, BollingerOutput, BreadthThrust, Breakaway, BullishPercentIndex, Butterfly,
|
BetaNeutralSpread, BodySizePct, BollingerBands, BollingerBandwidth, BollingerOutput,
|
||||||
CalendarSpread, CalmarRatio, Camarilla, CamarillaPivotsOutput, Cci, CenterOfGravity, Cfo,
|
BreadthThrust, Breakaway, BullishPercentIndex, Butterfly, CalendarSpread, CalmarRatio,
|
||||||
ChaikinMoneyFlow, ChaikinOscillator, ChaikinVolatility, ChandeKrollStop, ChandeKrollStopOutput,
|
Camarilla, CamarillaPivotsOutput, Cci, CenterOfGravity, Cfo, ChaikinMoneyFlow,
|
||||||
ChandelierExit, ChandelierExitOutput, ChoppinessIndex, ClassicPivots, ClassicPivotsOutput,
|
ChaikinOscillator, ChaikinVolatility, ChandeKrollStop, ChandeKrollStopOutput, ChandelierExit,
|
||||||
CloseVsOpen, ClosingMarubozu, Cmo, CoefficientOfVariation, Cointegration, CointegrationOutput,
|
ChandelierExitOutput, ChoppinessIndex, ClassicPivots, ClassicPivotsOutput, CloseVsOpen,
|
||||||
|
ClosingMarubozu, Cmo, CoefficientOfVariation, Cointegration, CointegrationOutput,
|
||||||
ConcealingBabySwallow, ConditionalValueAtRisk, ConnorsRsi, Coppock, Counterattack, Crab,
|
ConcealingBabySwallow, ConditionalValueAtRisk, ConnorsRsi, Coppock, Counterattack, Crab,
|
||||||
CumulativeVolumeDelta, CumulativeVolumeIndex, CupAndHandle, CyberneticCycle, Cypher,
|
CumulativeVolumeDelta, CumulativeVolumeIndex, CupAndHandle, CyberneticCycle, Cypher,
|
||||||
DayOfWeekProfile, DayOfWeekProfileOutput, Decycler, DecyclerOscillator, Dema, DemandIndex,
|
DayOfWeekProfile, DayOfWeekProfileOutput, Decycler, DecyclerOscillator, Dema, DemandIndex,
|
||||||
DemarkPivots, DemarkPivotsOutput, DepthSlope, DetrendedStdDev, DistanceSsd, Doji, DojiStar,
|
DemarkPivots, DemarkPivotsOutput, DepthSlope, DetrendedStdDev, DistanceSsd, Doji, DojiStar,
|
||||||
Donchian, DonchianOutput, DonchianStop, DonchianStopOutput, DoubleBollinger,
|
Donchian, DonchianOutput, DonchianStop, DonchianStopOutput, DoubleBollinger,
|
||||||
DoubleBollingerOutput, DoubleTopBottom, DownsideGapThreeMethods, Dpo, DragonflyDoji,
|
DoubleBollingerOutput, DoubleTopBottom, DownsideGapThreeMethods, Dpo, DragonflyDoji,
|
||||||
DrawdownDuration, Dx, EaseOfMovement, EffectiveSpread, EhlersStochastic, ElderImpulse, Ema,
|
DrawdownDuration, Dx, EaseOfMovement, EffectiveSpread, EhlersStochastic, Ehma, ElderImpulse,
|
||||||
EmpiricalModeDecomposition, Engulfing, EveningDojiStar, Evwma, Expectancy, FallingThreeMethods,
|
Ema, EmpiricalModeDecomposition, Engulfing, EveningDojiStar, Evwma, Expectancy,
|
||||||
Fama, FibArcs, FibArcsOutput, FibChannel, FibChannelOutput, FibConfluence, FibConfluenceOutput,
|
FallingThreeMethods, Fama, FibArcs, FibArcsOutput, FibChannel, FibChannelOutput, FibConfluence,
|
||||||
FibExtension, FibExtensionOutput, FibFan, FibFanOutput, FibProjection, FibProjectionOutput,
|
FibConfluenceOutput, FibExtension, FibExtensionOutput, FibFan, FibFanOutput, FibProjection,
|
||||||
FibRetracement, FibRetracementOutput, FibTimeZones, FibTimeZonesOutput, FibonacciPivots,
|
FibProjectionOutput, FibRetracement, FibRetracementOutput, FibTimeZones, FibTimeZonesOutput,
|
||||||
FibonacciPivotsOutput, FisherTransform, FlagPennant, Footprint, FootprintOutput, ForceIndex,
|
FibonacciPivots, FibonacciPivotsOutput, FisherTransform, FlagPennant, Footprint,
|
||||||
FractalChaosBands, FractalChaosBandsOutput, Frama, FundingBasis, FundingRate, FundingRateMean,
|
FootprintOutput, ForceIndex, FractalChaosBands, FractalChaosBandsOutput, Frama, FundingBasis,
|
||||||
FundingRateZScore, GainLossRatio, GapSideBySideWhite, GarmanKlassVolatility, Gartley,
|
FundingRate, FundingRateMean, FundingRateZScore, GainLossRatio, GapSideBySideWhite,
|
||||||
GoldenPocket, GoldenPocketOutput, GrangerCausality, GravestoneDoji, Hammer, HangingMan, Harami,
|
GarmanKlassVolatility, Gartley, GeneralizedDema, GeometricMa, GoldenPocket, GoldenPocketOutput,
|
||||||
HeadAndShoulders, HeikinAshi, HeikinAshiOutput, HiLoActivator, HighLowIndex, HighLowRange,
|
GrangerCausality, GravestoneDoji, Hammer, HangingMan, Harami, HeadAndShoulders, HeikinAshi,
|
||||||
HighWave, Hikkake, HikkakeModified, HilbertDominantCycle, HistoricalVolatility, Hma,
|
HeikinAshiOutput, HiLoActivator, HighLowIndex, HighLowRange, HighWave, Hikkake,
|
||||||
HomingPigeon, HtDcPhase, HtPhasor, HtPhasorOutput, HtTrendMode, HurstChannel,
|
HikkakeModified, HilbertDominantCycle, HistoricalVolatility, Hma, HoltWinters, HomingPigeon,
|
||||||
HurstChannelOutput, HurstExponent, Ichimoku, IchimokuOutput, IdenticalThreeCrows, InNeck,
|
HtDcPhase, HtPhasor, HtPhasorOutput, HtTrendMode, HurstChannel, HurstChannelOutput,
|
||||||
Inertia, InformationRatio, InitialBalance, InitialBalanceOutput, InstantaneousTrendline,
|
HurstExponent, Ichimoku, IchimokuOutput, IdenticalThreeCrows, InNeck, Inertia,
|
||||||
|
InformationRatio, InitialBalance, InitialBalanceOutput, InstantaneousTrendline,
|
||||||
IntradayVolatilityProfile, IntradayVolatilityProfileOutput, InverseFisherTransform,
|
IntradayVolatilityProfile, IntradayVolatilityProfileOutput, InverseFisherTransform,
|
||||||
InvertedHammer, Jma, JumpIndicator, KagiBars, KalmanHedgeRatio, KalmanHedgeRatioOutput, Kama,
|
InvertedHammer, Jma, JumpIndicator, KagiBars, KalmanHedgeRatio, KalmanHedgeRatioOutput, Kama,
|
||||||
KellyCriterion, Keltner, KeltnerOutput, Kicking, KickingByLength, Kst, KstOutput, Kurtosis,
|
KellyCriterion, Keltner, KeltnerOutput, Kicking, KickingByLength, Kst, KstOutput, Kurtosis,
|
||||||
@@ -97,27 +99,28 @@ pub use indicators::{
|
|||||||
LogReturn, LongLeggedDoji, LongLine, LongShortRatio, MaEnvelope, MaEnvelopeOutput, MacdExt,
|
LogReturn, LongLeggedDoji, LongLine, LongShortRatio, MaEnvelope, MaEnvelopeOutput, MacdExt,
|
||||||
MacdFix, MacdIndicator, MacdOutput, Mama, MamaOutput, MarketFacilitationIndex, Marubozu,
|
MacdFix, MacdIndicator, MacdOutput, Mama, MamaOutput, MarketFacilitationIndex, Marubozu,
|
||||||
MassIndex, MatHold, MatchingLow, MaxDrawdown, McClellanOscillator, McClellanSummationIndex,
|
MassIndex, MatHold, MatchingLow, MaxDrawdown, McClellanOscillator, McClellanSummationIndex,
|
||||||
McGinleyDynamic, MedianAbsoluteDeviation, MedianPrice, Mfi, Microprice, MidPoint, MidPrice,
|
McGinleyDynamic, MedianAbsoluteDeviation, MedianMa, MedianPrice, Mfi, Microprice, MidPoint,
|
||||||
MinusDi, MinusDm, Mom, MorningDojiStar, MorningEveningStar, Natr, NewHighsNewLows, Nvi,
|
MidPrice, MinusDi, MinusDm, Mom, MorningDojiStar, MorningEveningStar, Natr, NewHighsNewLows,
|
||||||
OIPriceDivergence, OIWeighted, Obv, OmegaRatio, OnNeck, OpenInterestDelta, OpeningMarubozu,
|
Nvi, OIPriceDivergence, OIWeighted, Obv, OmegaRatio, OnNeck, OpenInterestDelta,
|
||||||
OpeningRange, OpeningRangeOutput, OrderBookImbalanceFull, OrderBookImbalanceTop1,
|
OpeningMarubozu, OpeningRange, OpeningRangeOutput, OrderBookImbalanceFull,
|
||||||
OrderBookImbalanceTopN, OrderFlowImbalance, OuHalfLife, OvernightGap, OvernightIntradayReturn,
|
OrderBookImbalanceTop1, OrderBookImbalanceTopN, OrderFlowImbalance, OuHalfLife, OvernightGap,
|
||||||
OvernightIntradayReturnOutput, PainIndex, PairSpreadZScore, PairwiseBeta, ParkinsonVolatility,
|
OvernightIntradayReturn, OvernightIntradayReturnOutput, PainIndex, PairSpreadZScore,
|
||||||
PearsonCorrelation, PercentAboveMa, PercentB, PercentageTrailingStop, Pgo, PiercingDarkCloud,
|
PairwiseBeta, ParkinsonVolatility, PearsonCorrelation, PercentAboveMa, PercentB,
|
||||||
PlusDi, PlusDm, Pmo, PointAndFigureBars, Ppo, ProfitFactor, Psar, Pvi, QuotedSpread, RSquared,
|
PercentageTrailingStop, Pgo, PiercingDarkCloud, PlusDi, PlusDm, Pmo, PointAndFigureBars, Ppo,
|
||||||
RealizedSpread, RealizedVolatility, RecoveryFactor, RectangleRange, RegimeLabel,
|
ProfitFactor, Psar, Pvi, QuotedSpread, RSquared, RealizedSpread, RealizedVolatility,
|
||||||
RelativeStrengthAB, RelativeStrengthOutput, RenkoBars, RenkoTrailingStop, RickshawMan,
|
RecoveryFactor, RectangleRange, RegimeLabel, RelativeStrengthAB, RelativeStrengthOutput,
|
||||||
RisingThreeMethods, Roc, Rocp, Rocr, Rocr100, RogersSatchellVolatility, RollMeasure,
|
RenkoBars, RenkoTrailingStop, RickshawMan, RisingThreeMethods, Roc, Rocp, Rocr, Rocr100,
|
||||||
RollingCorrelation, RollingCovariance, RollingIqr, RollingPercentileRank, RollingQuantile,
|
RogersSatchellVolatility, RollMeasure, RollingCorrelation, RollingCovariance, RollingIqr,
|
||||||
RollingVwap, RoofingFilter, Rsi, Rvi, RviVolatility, Rwi, RwiOutput, SarExt, SeasonalZScore,
|
RollingPercentileRank, RollingQuantile, RollingVwap, RoofingFilter, Rsi, Rvi, RviVolatility,
|
||||||
SeparatingLines, SessionHighLow, SessionHighLowOutput, SessionRange, SessionRangeOutput,
|
Rwi, RwiOutput, SarExt, SeasonalZScore, SeparatingLines, SessionHighLow, SessionHighLowOutput,
|
||||||
SessionVwap, Shark, SharpeRatio, ShootingStar, ShortLine, SignedVolume, SineWave, Skewness,
|
SessionRange, SessionRangeOutput, SessionVwap, Shark, SharpeRatio, ShootingStar, ShortLine,
|
||||||
Sma, Smi, Smma, SortinoRatio, SpearmanCorrelation, SpinningTop, SpreadAr1Coefficient,
|
SignedVolume, SineWave, SineWeightedMa, Skewness, Sma, Smi, Smma, SortinoRatio,
|
||||||
SpreadBollingerBands, SpreadBollingerBandsOutput, SpreadHurst, StalledPattern, StandardError,
|
SpearmanCorrelation, SpinningTop, SpreadAr1Coefficient, SpreadBollingerBands,
|
||||||
StandardErrorBands, StandardErrorBandsOutput, StarcBands, StarcBandsOutput, Stc, StdDev,
|
SpreadBollingerBandsOutput, SpreadHurst, StalledPattern, StandardError, StandardErrorBands,
|
||||||
StepTrailingStop, StickSandwich, StochRsi, Stochastic, StochasticOutput, SuperSmoother,
|
StandardErrorBandsOutput, StarcBands, StarcBandsOutput, Stc, StdDev, StepTrailingStop,
|
||||||
SuperTrend, SuperTrendOutput, TakerBuySellRatio, Takuri, TasukiGap, TdCombo, TdCountdown,
|
StickSandwich, StochRsi, Stochastic, StochasticOutput, SuperSmoother, SuperTrend,
|
||||||
TdDeMarker, TdDifferential, TdLines, TdLinesOutput, TdOpen, TdPressure, TdRangeProjection,
|
SuperTrendOutput, TakerBuySellRatio, Takuri, TasukiGap, TdCombo, TdCountdown, TdDeMarker,
|
||||||
|
TdDifferential, TdLines, TdLinesOutput, TdOpen, TdPressure, TdRangeProjection,
|
||||||
TdRangeProjectionOutput, TdRei, TdRiskLevel, TdRiskLevelOutput, TdSequential,
|
TdRangeProjectionOutput, TdRei, TdRiskLevel, TdRiskLevelOutput, TdSequential,
|
||||||
TdSequentialOutput, TdSetup, Tema, TermStructureBasis, ThreeDrives, ThreeInside,
|
TdSequentialOutput, TdSetup, Tema, TermStructureBasis, ThreeDrives, ThreeInside,
|
||||||
ThreeLineStrike, ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth, Thrusting, TickIndex,
|
ThreeLineStrike, ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth, Thrusting, TickIndex,
|
||||||
|
|||||||
+1
-1
@@ -8,7 +8,7 @@ That includes:
|
|||||||
[Python](https://docs.wickra.org/Quickstart-Python),
|
[Python](https://docs.wickra.org/Quickstart-Python),
|
||||||
[Node](https://docs.wickra.org/Quickstart-Node), and
|
[Node](https://docs.wickra.org/Quickstart-Node), and
|
||||||
[WASM](https://docs.wickra.org/Quickstart-WASM).
|
[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 &
|
the sixteen families (Moving Averages, Momentum Oscillators, Trend &
|
||||||
Directional, Price Oscillators, Volatility & Bands, Bands & Channels,
|
Directional, Price Oscillators, Volatility & Bands, Bands & Channels,
|
||||||
Trailing Stops, Volume, Price Statistics, Ehlers / Cycle DSP, Pivots &
|
Trailing Stops, Volume, Price Statistics, Ehlers / Cycle DSP, Pivots &
|
||||||
|
|||||||
Generated
+7
-7
@@ -17,7 +17,7 @@
|
|||||||
},
|
},
|
||||||
"../../bindings/node": {
|
"../../bindings/node": {
|
||||||
"name": "wickra",
|
"name": "wickra",
|
||||||
"version": "0.5.4",
|
"version": "0.5.5",
|
||||||
"license": "MIT OR Apache-2.0",
|
"license": "MIT OR Apache-2.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@napi-rs/cli": "^2.18.0"
|
"@napi-rs/cli": "^2.18.0"
|
||||||
@@ -26,12 +26,12 @@
|
|||||||
"node": ">= 18"
|
"node": ">= 18"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"wickra-darwin-arm64": "0.5.4",
|
"wickra-darwin-arm64": "0.5.5",
|
||||||
"wickra-darwin-x64": "0.5.4",
|
"wickra-darwin-x64": "0.5.5",
|
||||||
"wickra-linux-arm64-gnu": "0.5.4",
|
"wickra-linux-arm64-gnu": "0.5.5",
|
||||||
"wickra-linux-x64-gnu": "0.5.4",
|
"wickra-linux-x64-gnu": "0.5.5",
|
||||||
"wickra-win32-arm64-msvc": "0.5.4",
|
"wickra-win32-arm64-msvc": "0.5.5",
|
||||||
"wickra-win32-x64-msvc": "0.5.4"
|
"wickra-win32-x64-msvc": "0.5.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/wickra": {
|
"node_modules/wickra": {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
//! `Ema(20)`. This target now covers every scalar indicator in the catalogue.
|
//! `Ema(20)`. This target now covers every scalar indicator in the catalogue.
|
||||||
|
|
||||||
use libfuzzer_sys::fuzz_target;
|
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
|
/// Drive a single streaming + batch run through one scalar indicator. Marked
|
||||||
/// `#[inline(never)]` so a panic backtrace pin-points the specific indicator.
|
/// `#[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(|| Dema::new(14).unwrap(), &data);
|
||||||
drive(|| Tema::new(14).unwrap(), &data);
|
drive(|| Tema::new(14).unwrap(), &data);
|
||||||
drive(|| Hma::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(|| Roc::new(14).unwrap(), &data);
|
||||||
drive(|| Rocp::new(14).unwrap(), &data);
|
drive(|| Rocp::new(14).unwrap(), &data);
|
||||||
drive(|| Rocr::new(14).unwrap(), &data);
|
drive(|| Rocr::new(14).unwrap(), &data);
|
||||||
|
|||||||
Reference in New Issue
Block a user