Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3dfbc415c5 | |||
| 6b8c6a0e7f | |||
| db186b18d3 |
@@ -180,14 +180,14 @@ jobs:
|
||||
exit 0
|
||||
fi
|
||||
cd docs-count
|
||||
sed -i -E "s/[0-9]+ (streaming-first )?indicators/${n} \1indicators/g" index.md overview.md Indicators-Overview.md
|
||||
sed -i -E "s/[0-9]+ (streaming-first )?indicators/${n} \1indicators/g" index.md overview.md Indicators-Overview.md .vitepress/config.ts
|
||||
if git diff --quiet; then
|
||||
echo "Docs indicator count unchanged."
|
||||
exit 0
|
||||
fi
|
||||
git config user.name "wickra-bot"
|
||||
git config user.email "wickra-bot@users.noreply.github.com"
|
||||
git add index.md overview.md Indicators-Overview.md
|
||||
git add index.md overview.md Indicators-Overview.md .vitepress/config.ts
|
||||
git commit -m "chore: sync indicator count to ${n}"
|
||||
if ! git push 2>/dev/null; then
|
||||
echo "::warning::push to wickra-lib/wickra-docs failed — ABOUT_SYNC_TOKEN likely lacks write (findings P10.0a)."
|
||||
|
||||
+10
-1
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.6.0] - 2026-06-06
|
||||
- **Volatility Cone** — volatility cone: current realized volatility within its historical min/median/max envelope (`VolatilityCone`).
|
||||
- **VolatilityRatio** — Schwager's volatility ratio: true range over the EMA of prior true ranges (`VolatilityRatio`).
|
||||
- **BipowerVariation** — jump-robust realized bipower variation (pi/2 sum of adjacent absolute log-return products) (`BipowerVariation`).
|
||||
- **VolatilityOfVolatility** — vol-of-vol: sample stddev of a rolling realized-volatility series (`VolatilityOfVolatility`).
|
||||
- **Garch11** — GARCH(1,1) conditional volatility with a long-run-variance anchor (`Garch11`).
|
||||
- **EwmaVolatility** — RiskMetrics exponentially-weighted volatility of log returns (lambda decay) (`EwmaVolatility`).
|
||||
|
||||
## [0.5.9] - 2026-06-06
|
||||
|
||||
### Added
|
||||
@@ -1293,7 +1301,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
optional Binance live feed.
|
||||
- Bindings for Python, Node.js, and WebAssembly.
|
||||
|
||||
[Unreleased]: https://github.com/wickra-lib/wickra/compare/v0.5.9...HEAD
|
||||
[Unreleased]: https://github.com/wickra-lib/wickra/compare/v0.6.0...HEAD
|
||||
[0.6.0]: https://github.com/wickra-lib/wickra/compare/v0.5.9...v0.6.0
|
||||
[0.5.9]: https://github.com/wickra-lib/wickra/compare/v0.5.8...v0.5.9
|
||||
[0.5.8]: https://github.com/wickra-lib/wickra/compare/v0.5.7...v0.5.8
|
||||
[0.5.7]: https://github.com/wickra-lib/wickra/compare/v0.5.6...v0.5.7
|
||||
|
||||
Generated
+8
-8
@@ -1944,7 +1944,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra"
|
||||
version = "0.5.9"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"approx",
|
||||
"criterion",
|
||||
@@ -1955,7 +1955,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-bench"
|
||||
version = "0.5.9"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"criterion",
|
||||
"kand",
|
||||
@@ -1967,7 +1967,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-core"
|
||||
version = "0.5.9"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"approx",
|
||||
"proptest",
|
||||
@@ -1977,7 +1977,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-data"
|
||||
version = "0.5.9"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"approx",
|
||||
"csv",
|
||||
@@ -1994,7 +1994,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-examples"
|
||||
version = "0.5.9"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
"tokio",
|
||||
@@ -2004,7 +2004,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-node"
|
||||
version = "0.5.9"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"napi",
|
||||
"napi-build",
|
||||
@@ -2014,7 +2014,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-python"
|
||||
version = "0.5.9"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"numpy",
|
||||
"pyo3",
|
||||
@@ -2023,7 +2023,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-wasm"
|
||||
version = "0.5.9"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"js-sys",
|
||||
|
||||
+2
-2
@@ -13,7 +13,7 @@ members = [
|
||||
exclude = ["fuzz"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.5.9"
|
||||
version = "0.6.0"
|
||||
authors = ["kingchenc <support@wickra.org>"]
|
||||
edition = "2021"
|
||||
rust-version = "1.86"
|
||||
@@ -25,7 +25,7 @@ keywords = ["finance", "trading", "indicators", "technical-analysis", "ta"]
|
||||
categories = ["finance", "mathematics", "science"]
|
||||
|
||||
[workspace.dependencies]
|
||||
wickra-core = { path = "crates/wickra-core", version = "0.5.9" }
|
||||
wickra-core = { path = "crates/wickra-core", version = "0.6.0" }
|
||||
|
||||
thiserror = "2"
|
||||
rayon = "1.10"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<p align="center">
|
||||
<a href="https://wickra.org"><img src="https://raw.githubusercontent.com/wickra-lib/.github/main/profile/wickra-banner.webp?v=423" 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=429" alt="Wickra — streaming-first technical indicators" width="100%"></a>
|
||||
</p>
|
||||
|
||||
[](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml)
|
||||
@@ -48,7 +48,7 @@ Full documentation lives at **[docs.wickra.org](https://docs.wickra.org)**:
|
||||
[Node](https://docs.wickra.org/Quickstart-Node),
|
||||
[WASM](https://docs.wickra.org/Quickstart-WASM).
|
||||
- **Indicators** — a per-indicator deep dive (formula, parameters, warmup) for
|
||||
every one of the 423 indicators; start at the
|
||||
every one of the 429 indicators; start at the
|
||||
[indicators overview](https://docs.wickra.org/Indicators-Overview).
|
||||
- **Reference** — [warmup periods](https://docs.wickra.org/Warmup-Periods),
|
||||
[streaming vs batch](https://docs.wickra.org/Streaming-vs-Batch),
|
||||
@@ -79,7 +79,7 @@ Plenty of TA libraries are fast. Each one forces a trade-off Wickra does not:
|
||||
| finta | clean | no | Python | ~80 | stale |
|
||||
| talipp | clean | yes | Python | ~40 | yes |
|
||||
|
||||
Wickra's edge is **breadth with reach**: 423 indicators that all update in O(1)
|
||||
Wickra's edge is **breadth with reach**: 429 indicators that all update in O(1)
|
||||
per tick and ship natively to Python, Node.js, WebAssembly and Rust from a
|
||||
single engine.
|
||||
|
||||
@@ -188,7 +188,7 @@ python -m benchmarks.compare_libraries
|
||||
|
||||
## Indicators
|
||||
|
||||
423 streaming-first indicators across twenty-four families. Every one passes the
|
||||
429 streaming-first indicators across twenty-four families. Every one passes the
|
||||
`batch == streaming` equivalence test, reference-value tests, and reset
|
||||
semantics tests. Each has a per-indicator deep dive (formula, parameters,
|
||||
warmup) at [docs.wickra.org](https://docs.wickra.org/Indicators-Overview).
|
||||
@@ -199,7 +199,7 @@ warmup) at [docs.wickra.org](https://docs.wickra.org/Indicators-Overview).
|
||||
| Momentum Oscillators | RSI (Wilder), Anchored RSI, Stochastic, CCI, ROC, Williams %R, MFI, Awesome Oscillator, MOM, CMO, TSI, PMO, StochRSI, Ultimate Oscillator, RVI, PGO, KST, SMI, Laguerre RSI, Connors RSI, Inertia, ROC Percentage (ROCP), ROC Ratio (ROCR), ROC Ratio 100 (ROCR100), Disparity Index, Fisher RSI, RSX, Dynamic Momentum Index, Stochastic CCI, RMI, Derivative Oscillator, Elder Ray, Intraday Momentum Index, QQE |
|
||||
| Trend & Directional | MACD, MACD Fixed (MACDFIX), MACD Extended (MACDEXT), ADX (+DI/-DI), ADXR, Aroon, TRIX, Aroon Oscillator, Vortex, Random Walk Index, Trend Intensity Index, Wave Trend Oscillator, Mass Index, Choppiness Index, Vertical Horizontal Filter, Plus DM, Minus DM, Plus DI, Minus DI, DX, TTM Trend, Trend Strength Index, Qstick, Polarized Fractal Efficiency, Wave PM, Gator Oscillator, Kase Permission Stochastic |
|
||||
| Price Oscillators | PPO, DPO, Coppock, Accelerator Oscillator, Balance of Power, APO, AO Histogram, CFO, Zero-Lag MACD, Elder Impulse, STC, TSF Oscillator, MACD Histogram, PPO Histogram |
|
||||
| Volatility & Bands | ATR, Bollinger Bands, Keltner Channels, Donchian Channels, NATR, StdDev, Ulcer Index, Historical Volatility, Bollinger Bandwidth, %B, True Range, Chaikin Volatility, RVI (Relative Volatility Index), Parkinson Volatility, Garman-Klass Volatility, Rogers-Satchell Volatility, Yang-Zhang Volatility |
|
||||
| Volatility & Bands | ATR, Bollinger Bands, Keltner Channels, Donchian Channels, NATR, StdDev, Ulcer Index, Historical Volatility, Bollinger Bandwidth, %B, True Range, Chaikin Volatility, RVI (Relative Volatility Index), Parkinson Volatility, Garman-Klass Volatility, Rogers-Satchell Volatility, Yang-Zhang Volatility, Volatility Cone |
|
||||
| Bands & Channels | MA Envelope, Acceleration Bands, STARC Bands, ATR Bands, Hurst Channel, LinReg Channel, Standard Error Bands, Double Bollinger Bands, TTM Squeeze, Fractal Chaos Bands, VWAP StdDev Bands |
|
||||
| Trailing Stops | Parabolic SAR, Parabolic SAR Extended (SAREXT), SuperTrend, Chandelier Exit, Chande Kroll Stop, ATR Trailing Stop, HiLo Activator, Volty Stop, Yo-Yo Exit, Donchian Channel Stop, Percentage Trailing Stop, Step Trailing Stop, Renko Trailing Stop |
|
||||
| Volume | OBV, VWAP (cumulative + rolling), ADL, Volume-Price Trend, Chaikin Money Flow, Chaikin Oscillator, Force Index, Ease of Movement, Klinger Volume Oscillator, Volume Oscillator, NVI, PVI, Williams A/D, Anchored VWAP, Demand Index, TSV, VZO, Market Facilitation Index |
|
||||
@@ -297,7 +297,7 @@ A Python live-trading example using the public `websockets` package lives at
|
||||
```
|
||||
wickra/
|
||||
├── crates/
|
||||
│ ├── wickra-core/ core engine + all 423 indicators
|
||||
│ ├── wickra-core/ core engine + all 429 indicators
|
||||
│ ├── wickra/ top-level facade crate (publishes on crates.io) + benches/
|
||||
│ ├── wickra-data/ CSV reader, tick aggregator, live exchange feeds
|
||||
│ └── wickra-bench/ internal cross-library benchmark harness (not published)
|
||||
@@ -428,8 +428,8 @@ The library is provided **as is**, without warranty of any kind; see
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/wickra-lib/wickra">
|
||||
<img alt="Star Wickra on GitHub"
|
||||
src="https://img.shields.io/badge/%E2%AD%90%20Star%20Wickra%20on%20GitHub-1f2328?style=for-the-badge&logo=github&logoColor=ffd866&labelColor=1f2328">
|
||||
<a href="https://star-history.com/#wickra-lib/wickra&Date">
|
||||
<img alt="Wickra star history" width="640"
|
||||
src="https://api.star-history.com/svg?repos=wickra-lib/wickra&type=Date&theme=dark">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
@@ -28,6 +28,10 @@ function num(v) {
|
||||
// --- Scalar indicators: update(value) vs batch(prices) ---
|
||||
|
||||
const scalarFactories = {
|
||||
BipowerVariation: () => new wickra.BipowerVariation(20),
|
||||
VolatilityOfVolatility: () => new wickra.VolatilityOfVolatility(20, 20),
|
||||
Garch11: () => new wickra.Garch11(0.000002, 0.1, 0.88),
|
||||
EwmaVolatility: () => new wickra.EwmaVolatility(0.94),
|
||||
PpoHistogram: () => new wickra.PpoHistogram(3, 6, 3),
|
||||
MacdHistogram: () => new wickra.MacdHistogram(3, 6, 3),
|
||||
TsfOscillator: () => new wickra.TsfOscillator(3),
|
||||
@@ -350,6 +354,7 @@ const candleScalar = {
|
||||
IMI: { make: () => new wickra.IMI(14), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
TTM_TREND: { make: () => new wickra.TTM_TREND(6), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
|
||||
Qstick: { make: () => new wickra.Qstick(10), step: (ind, i) => ind.update(open[i], close[i]), batch: (ind) => ind.batch(open, close) },
|
||||
VolatilityRatio: { make: () => new wickra.VolatilityRatio(14), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
|
||||
};
|
||||
|
||||
for (const [name, d] of Object.entries(candleScalar)) {
|
||||
@@ -436,6 +441,7 @@ const multi = {
|
||||
QQE: { make: () => new wickra.QQE(14, 5, 4.236), fields: ['rsiMa', 'trailingLine'], step: (ind, i) => ind.update(close[i]), batch: (ind) => ind.batch(close) },
|
||||
GatorOscillator: { make: () => new wickra.GatorOscillator(13, 8, 5), fields: ['upper', 'lower'], step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
|
||||
KasePermissionStochastic: { make: () => new wickra.KasePermissionStochastic(9, 3), fields: ['fast', 'slow'], step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
|
||||
VolatilityCone: { make: () => new wickra.VolatilityCone(20, 60), fields: ['current', 'min', 'median', 'max', 'percentile'], step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
|
||||
};
|
||||
|
||||
for (const [name, d] of Object.entries(multi)) {
|
||||
|
||||
Vendored
+65
@@ -5,6 +5,17 @@
|
||||
|
||||
/** Library version (matches the Rust crate version). */
|
||||
export declare function version(): string
|
||||
/**
|
||||
* Volatility-cone result: current realized volatility and its lookback
|
||||
* envelope (min / median / max) plus the percentile rank of `current`.
|
||||
*/
|
||||
export interface VolatilityConeValue {
|
||||
current: number
|
||||
min: number
|
||||
median: number
|
||||
max: number
|
||||
percentile: number
|
||||
}
|
||||
/** Lead/lag result: the offset that maximises correlation, and that correlation. */
|
||||
export interface LeadLagValue {
|
||||
/** Offset that maximises `|corr(a, b shifted)|`. Positive ⇒ `a` leads `b`. */
|
||||
@@ -987,6 +998,51 @@ export declare class TsfOscillator {
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type BipowerVariationNode = BipowerVariation
|
||||
export declare class BipowerVariation {
|
||||
constructor(period: number)
|
||||
update(value: number): number | null
|
||||
batch(prices: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type EwmaVolatilityNode = EwmaVolatility
|
||||
export declare class EwmaVolatility {
|
||||
constructor(lambda: number)
|
||||
update(value: number): number | null
|
||||
batch(prices: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type Garch11Node = Garch11
|
||||
export declare class Garch11 {
|
||||
constructor(omega: number, alpha: number, beta: number)
|
||||
update(value: number): number | null
|
||||
batch(prices: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type VolatilityOfVolatilityNode = VolatilityOfVolatility
|
||||
export declare class VolatilityOfVolatility {
|
||||
constructor(volWindow: number, vovWindow: number)
|
||||
update(value: number): number | null
|
||||
batch(prices: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type VolatilityConeNode = VolatilityCone
|
||||
export declare class VolatilityCone {
|
||||
constructor(window: number, lookback: number)
|
||||
update(high: number, low: number, close: number): VolatilityConeValue | null
|
||||
batch(high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type JumpIndicatorNode = JumpIndicator
|
||||
export declare class JumpIndicator {
|
||||
constructor(period: number, threshold: number)
|
||||
@@ -1574,6 +1630,15 @@ export declare class KasePermissionStochastic {
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type VolatilityRatioNode = VolatilityRatio
|
||||
export declare class VolatilityRatio {
|
||||
constructor(period: number)
|
||||
update(high: number, low: number, close: number): number | null
|
||||
batch(high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type StochNode = Stochastic
|
||||
export declare class Stochastic {
|
||||
constructor(kPeriod: number, dPeriod: number)
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wickra-darwin-arm64",
|
||||
"version": "0.5.9",
|
||||
"version": "0.6.0",
|
||||
"description": "Native binding for wickra (macOS Apple Silicon). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||
"main": "wickra.darwin-arm64.node",
|
||||
"files": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wickra-darwin-x64",
|
||||
"version": "0.5.9",
|
||||
"version": "0.6.0",
|
||||
"description": "Native binding for wickra (macOS Intel). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||
"main": "wickra.darwin-x64.node",
|
||||
"files": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wickra-linux-arm64-gnu",
|
||||
"version": "0.5.9",
|
||||
"version": "0.6.0",
|
||||
"description": "Native binding for wickra (linux arm64 GNU). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||
"main": "wickra.linux-arm64-gnu.node",
|
||||
"files": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wickra-linux-x64-gnu",
|
||||
"version": "0.5.9",
|
||||
"version": "0.6.0",
|
||||
"description": "Native binding for wickra (linux x64 GNU). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||
"main": "wickra.linux-x64-gnu.node",
|
||||
"files": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wickra-win32-arm64-msvc",
|
||||
"version": "0.5.9",
|
||||
"version": "0.6.0",
|
||||
"description": "Native binding for wickra (Windows arm64 MSVC). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||
"main": "wickra.win32-arm64-msvc.node",
|
||||
"files": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wickra-win32-x64-msvc",
|
||||
"version": "0.5.9",
|
||||
"version": "0.6.0",
|
||||
"description": "Native binding for wickra (Windows x64 MSVC). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||
"main": "wickra.win32-x64-msvc.node",
|
||||
"files": [
|
||||
|
||||
Generated
+20
-20
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "wickra",
|
||||
"version": "0.5.9",
|
||||
"version": "0.6.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "wickra",
|
||||
"version": "0.5.9",
|
||||
"version": "0.6.0",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"devDependencies": {
|
||||
"@napi-rs/cli": "^2.18.0"
|
||||
@@ -15,12 +15,12 @@
|
||||
"node": ">= 18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"wickra-darwin-arm64": "0.5.9",
|
||||
"wickra-darwin-x64": "0.5.9",
|
||||
"wickra-linux-arm64-gnu": "0.5.9",
|
||||
"wickra-linux-x64-gnu": "0.5.9",
|
||||
"wickra-win32-arm64-msvc": "0.5.9",
|
||||
"wickra-win32-x64-msvc": "0.5.9"
|
||||
"wickra-darwin-arm64": "0.6.0",
|
||||
"wickra-darwin-x64": "0.6.0",
|
||||
"wickra-linux-arm64-gnu": "0.6.0",
|
||||
"wickra-linux-x64-gnu": "0.6.0",
|
||||
"wickra-win32-arm64-msvc": "0.6.0",
|
||||
"wickra-win32-x64-msvc": "0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/cli": {
|
||||
@@ -41,8 +41,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wickra-darwin-arm64": {
|
||||
"version": "0.5.9",
|
||||
"resolved": "https://registry.npmjs.org/wickra-darwin-arm64/-/wickra-darwin-arm64-0.5.9.tgz",
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/wickra-darwin-arm64/-/wickra-darwin-arm64-0.6.0.tgz",
|
||||
"integrity": "sha512-4eZiBR/yGUdr4nzhEUFy2i69XgNx64iI2ax/LPamsThgylC0KpHOZKK19QzJ2d9KbK4C8nMjME5FLuR+4GNEwQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
@@ -57,8 +57,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wickra-darwin-x64": {
|
||||
"version": "0.5.9",
|
||||
"resolved": "https://registry.npmjs.org/wickra-darwin-x64/-/wickra-darwin-x64-0.5.9.tgz",
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/wickra-darwin-x64/-/wickra-darwin-x64-0.6.0.tgz",
|
||||
"integrity": "sha512-6hf8zI3QPjTFp4zCpmgUwDvNtu6jHqNUHKD5e55POo0CgA52HkpyxSPtVm8TGTIZDI7kPjlbOdBM8CJ76mmXwA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
@@ -73,8 +73,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wickra-linux-arm64-gnu": {
|
||||
"version": "0.5.9",
|
||||
"resolved": "https://registry.npmjs.org/wickra-linux-arm64-gnu/-/wickra-linux-arm64-gnu-0.5.9.tgz",
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/wickra-linux-arm64-gnu/-/wickra-linux-arm64-gnu-0.6.0.tgz",
|
||||
"integrity": "sha512-kSe6y0xBMSiqdPLXNjwop5WZdHtvdBNKSEBCwZ4hFq33p4apW25/wrlzv9/oDuyD4kuPabJEhCCnFOplh58CUg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
@@ -89,8 +89,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wickra-linux-x64-gnu": {
|
||||
"version": "0.5.9",
|
||||
"resolved": "https://registry.npmjs.org/wickra-linux-x64-gnu/-/wickra-linux-x64-gnu-0.5.9.tgz",
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/wickra-linux-x64-gnu/-/wickra-linux-x64-gnu-0.6.0.tgz",
|
||||
"integrity": "sha512-tWBWS4qz7hxM4xnpFb59bhf6TaLwXq0Z3jEa/2l7r8PiHA94g8r8S53NRMiT+4yiL5hSWe/nUiC/YXdRrhEZ4g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
@@ -105,8 +105,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wickra-win32-arm64-msvc": {
|
||||
"version": "0.5.9",
|
||||
"resolved": "https://registry.npmjs.org/wickra-win32-arm64-msvc/-/wickra-win32-arm64-msvc-0.5.9.tgz",
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/wickra-win32-arm64-msvc/-/wickra-win32-arm64-msvc-0.6.0.tgz",
|
||||
"integrity": "sha512-EXIckHxAtF75PUGDKRzXyqMe9ldP0JjSdu68WFN6iJfp+McYrGu6h40TEJlQ/oUEIoPqiZB/xhVyo/el5Lg7zw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
@@ -121,8 +121,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wickra-win32-x64-msvc": {
|
||||
"version": "0.5.9",
|
||||
"resolved": "https://registry.npmjs.org/wickra-win32-x64-msvc/-/wickra-win32-x64-msvc-0.5.9.tgz",
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/wickra-win32-x64-msvc/-/wickra-win32-x64-msvc-0.6.0.tgz",
|
||||
"integrity": "sha512-Yfsqq1Xwp6hdxMyLze411vNdo7BDwI6+lPSe7A9XdqyPecNDbtKwYLpsal2r8EHbNzqM+R8XnuRtUaEQS5VlUQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wickra",
|
||||
"version": "0.5.9",
|
||||
"version": "0.6.0",
|
||||
"description": "Streaming-first technical indicators: incremental, fast, install-free. Node bindings powered by Rust.",
|
||||
"author": "kingchenc <support@wickra.org>",
|
||||
"main": "index.js",
|
||||
@@ -47,12 +47,12 @@
|
||||
"node": ">= 18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"wickra-linux-x64-gnu": "0.5.9",
|
||||
"wickra-linux-arm64-gnu": "0.5.9",
|
||||
"wickra-darwin-x64": "0.5.9",
|
||||
"wickra-darwin-arm64": "0.5.9",
|
||||
"wickra-win32-x64-msvc": "0.5.9",
|
||||
"wickra-win32-arm64-msvc": "0.5.9"
|
||||
"wickra-linux-x64-gnu": "0.6.0",
|
||||
"wickra-linux-arm64-gnu": "0.6.0",
|
||||
"wickra-darwin-x64": "0.6.0",
|
||||
"wickra-darwin-arm64": "0.6.0",
|
||||
"wickra-win32-x64-msvc": "0.6.0",
|
||||
"wickra-win32-arm64-msvc": "0.6.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "napi build --platform --release",
|
||||
|
||||
@@ -220,6 +220,199 @@ node_scalar_indicator!(
|
||||
wc::TrendStrengthIndex
|
||||
);
|
||||
node_scalar_indicator!(TsfOscillatorNode, "TsfOscillator", wc::TsfOscillator);
|
||||
node_scalar_indicator!(
|
||||
BipowerVariationNode,
|
||||
"BipowerVariation",
|
||||
wc::BipowerVariation
|
||||
);
|
||||
|
||||
#[napi(js_name = "EwmaVolatility")]
|
||||
pub struct EwmaVolatilityNode {
|
||||
inner: wc::EwmaVolatility,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl EwmaVolatilityNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(lambda: f64) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::EwmaVolatility::new(lambda).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(&mut self, value: f64) -> Option<f64> {
|
||||
self.inner.update(value)
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(&mut self, prices: Vec<f64>) -> Vec<f64> {
|
||||
flatten(self.inner.batch(&prices))
|
||||
}
|
||||
#[napi]
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
#[napi(js_name = "isReady")]
|
||||
pub fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
#[napi(js_name = "warmupPeriod")]
|
||||
pub fn warmup_period(&self) -> u32 {
|
||||
self.inner.warmup_period() as u32
|
||||
}
|
||||
}
|
||||
|
||||
#[napi(js_name = "Garch11")]
|
||||
pub struct Garch11Node {
|
||||
inner: wc::Garch11,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl Garch11Node {
|
||||
#[napi(constructor)]
|
||||
pub fn new(omega: f64, alpha: f64, beta: f64) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::Garch11::new(omega, 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
|
||||
}
|
||||
}
|
||||
|
||||
#[napi(js_name = "VolatilityOfVolatility")]
|
||||
pub struct VolatilityOfVolatilityNode {
|
||||
inner: wc::VolatilityOfVolatility,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl VolatilityOfVolatilityNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(vol_window: u32, vov_window: u32) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::VolatilityOfVolatility::new(vol_window as usize, vov_window as usize)
|
||||
.map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(&mut self, value: f64) -> Option<f64> {
|
||||
self.inner.update(value)
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(&mut self, prices: Vec<f64>) -> Vec<f64> {
|
||||
flatten(self.inner.batch(&prices))
|
||||
}
|
||||
#[napi]
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
#[napi(js_name = "isReady")]
|
||||
pub fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
#[napi(js_name = "warmupPeriod")]
|
||||
pub fn warmup_period(&self) -> u32 {
|
||||
self.inner.warmup_period() as u32
|
||||
}
|
||||
}
|
||||
|
||||
/// Volatility-cone result: current realized volatility and its lookback
|
||||
/// envelope (min / median / max) plus the percentile rank of `current`.
|
||||
#[napi(object)]
|
||||
pub struct VolatilityConeValue {
|
||||
pub current: f64,
|
||||
pub min: f64,
|
||||
pub median: f64,
|
||||
pub max: f64,
|
||||
pub percentile: f64,
|
||||
}
|
||||
|
||||
#[napi(js_name = "VolatilityCone")]
|
||||
pub struct VolatilityConeNode {
|
||||
inner: wc::VolatilityCone,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl VolatilityConeNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(window: u32, lookback: u32) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::VolatilityCone::new(window as usize, lookback as usize).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(
|
||||
&mut self,
|
||||
high: f64,
|
||||
low: f64,
|
||||
close: f64,
|
||||
) -> napi::Result<Option<VolatilityConeValue>> {
|
||||
Ok(self
|
||||
.inner
|
||||
.update(cnd(high, low, close, 0.0)?)
|
||||
.map(|o| VolatilityConeValue {
|
||||
current: o.current,
|
||||
min: o.min,
|
||||
median: o.median,
|
||||
max: o.max,
|
||||
percentile: o.percentile,
|
||||
}))
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
high: Vec<f64>,
|
||||
low: Vec<f64>,
|
||||
close: Vec<f64>,
|
||||
) -> napi::Result<Vec<f64>> {
|
||||
if high.len() != low.len() || low.len() != close.len() {
|
||||
return Err(NapiError::from_reason(
|
||||
"high, low, close must be equal length".to_string(),
|
||||
));
|
||||
}
|
||||
let n = high.len();
|
||||
let mut out = vec![f64::NAN; n * 5];
|
||||
for i in 0..n {
|
||||
if let Some(o) = self.inner.update(cnd(high[i], low[i], close[i], 0.0)?) {
|
||||
out[i * 5] = o.current;
|
||||
out[i * 5 + 1] = o.min;
|
||||
out[i * 5 + 2] = o.median;
|
||||
out[i * 5 + 3] = o.max;
|
||||
out[i * 5 + 4] = o.percentile;
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
#[napi]
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
#[napi(js_name = "isReady")]
|
||||
pub fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
#[napi(js_name = "warmupPeriod")]
|
||||
pub fn warmup_period(&self) -> u32 {
|
||||
self.inner.warmup_period() as u32
|
||||
}
|
||||
}
|
||||
|
||||
#[napi(js_name = "JumpIndicator")]
|
||||
pub struct JumpIndicatorNode {
|
||||
inner: wc::JumpIndicator,
|
||||
@@ -2623,6 +2816,59 @@ impl KasePermissionStochasticNode {
|
||||
}
|
||||
}
|
||||
|
||||
#[napi(js_name = "VolatilityRatio")]
|
||||
pub struct VolatilityRatioNode {
|
||||
inner: wc::VolatilityRatio,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl VolatilityRatioNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(period: u32) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::VolatilityRatio::new(period as usize).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(&mut self, high: f64, low: f64, close: f64) -> napi::Result<Option<f64>> {
|
||||
Ok(self.inner.update(cnd(high, low, close, 0.0)?))
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
high: Vec<f64>,
|
||||
low: Vec<f64>,
|
||||
close: Vec<f64>,
|
||||
) -> napi::Result<Vec<f64>> {
|
||||
if high.len() != low.len() || low.len() != close.len() {
|
||||
return Err(NapiError::from_reason(
|
||||
"high, low, close must be equal length".to_string(),
|
||||
));
|
||||
}
|
||||
let mut out = Vec::with_capacity(high.len());
|
||||
for i in 0..high.len() {
|
||||
out.push(
|
||||
self.inner
|
||||
.update(cnd(high[i], low[i], close[i], 0.0)?)
|
||||
.unwrap_or(f64::NAN),
|
||||
);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
#[napi]
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
#[napi(js_name = "isReady")]
|
||||
pub fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
#[napi(js_name = "warmupPeriod")]
|
||||
pub fn warmup_period(&self) -> u32 {
|
||||
self.inner.warmup_period() as u32
|
||||
}
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
pub struct StochValue {
|
||||
pub k: f64,
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "maturin"
|
||||
|
||||
[project]
|
||||
name = "wickra"
|
||||
version = "0.5.9"
|
||||
version = "0.6.0"
|
||||
description = "Streaming-first technical indicators: incremental, fast, install-free."
|
||||
readme = "README.md"
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
@@ -25,6 +25,12 @@ from __future__ import annotations
|
||||
|
||||
from ._wickra import (
|
||||
__version__,
|
||||
VolatilityCone,
|
||||
VolatilityRatio,
|
||||
BipowerVariation,
|
||||
VolatilityOfVolatility,
|
||||
Garch11,
|
||||
EwmaVolatility,
|
||||
PpoHistogram,
|
||||
MacdHistogram,
|
||||
TsfOscillator,
|
||||
@@ -476,6 +482,12 @@ from ._wickra import (
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"VolatilityCone",
|
||||
"VolatilityRatio",
|
||||
"BipowerVariation",
|
||||
"VolatilityOfVolatility",
|
||||
"Garch11",
|
||||
"EwmaVolatility",
|
||||
"PpoHistogram",
|
||||
"MacdHistogram",
|
||||
"TsfOscillator",
|
||||
|
||||
@@ -53,6 +53,8 @@ type PivotLevels = (f64, f64, f64, f64, f64, f64, f64);
|
||||
type FibExtLevels = (f64, f64, f64, f64, f64);
|
||||
/// `(pp, r1, r2, s1, s2)` pivot levels returned by Woodie pivots.
|
||||
type WoodieLevels = (f64, f64, f64, f64, f64);
|
||||
/// `(current, min, median, max, percentile)` volatility-cone envelope.
|
||||
type ConeBands = (f64, f64, f64, f64, f64);
|
||||
/// `(tenkan, kijun, senkou_a, senkou_b, chikou)` Ichimoku lines, each optional during warmup.
|
||||
type IchimokuLines = (
|
||||
Option<f64>,
|
||||
@@ -3433,6 +3435,130 @@ impl PyPpoHistogram {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== BipowerVariation ==============================
|
||||
|
||||
#[pyclass(
|
||||
name = "BipowerVariation",
|
||||
module = "wickra._wickra",
|
||||
skip_from_py_object
|
||||
)]
|
||||
#[derive(Clone)]
|
||||
struct PyBipowerVariation {
|
||||
inner: wc::BipowerVariation,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyBipowerVariation {
|
||||
#[new]
|
||||
#[pyo3(signature = (period=20))]
|
||||
fn new(period: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::BipowerVariation::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!("BipowerVariation(period={})", self.inner.period())
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== VolatilityRatio ==============================
|
||||
|
||||
#[pyclass(
|
||||
name = "VolatilityRatio",
|
||||
module = "wickra._wickra",
|
||||
skip_from_py_object
|
||||
)]
|
||||
#[derive(Clone)]
|
||||
struct PyVolatilityRatio {
|
||||
inner: wc::VolatilityRatio,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyVolatilityRatio {
|
||||
#[new]
|
||||
#[pyo3(signature = (period=14))]
|
||||
fn new(period: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::VolatilityRatio::new(period).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult<Option<f64>> {
|
||||
let c = extract_candle(candle)?;
|
||||
Ok(self.inner.update(c))
|
||||
}
|
||||
/// Batch over numpy columns: high, low, close (all 1-D, equal length).
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
let h = high
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let l = low
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let c = close
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
if h.len() != l.len() || l.len() != c.len() {
|
||||
return Err(PyValueError::new_err(
|
||||
"high, low, close must be equal length",
|
||||
));
|
||||
}
|
||||
let mut out = Vec::with_capacity(h.len());
|
||||
for i in 0..h.len() {
|
||||
let candle = wc::Candle::new(c[i], h[i], l[i], c[i], 0.0, 0).map_err(map_err)?;
|
||||
out.push(self.inner.update(candle).unwrap_or(f64::NAN));
|
||||
}
|
||||
Ok(out.into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn period(&self) -> usize {
|
||||
self.inner.period()
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.inner.warmup_period()
|
||||
}
|
||||
fn __repr__(&self) -> String {
|
||||
format!("VolatilityRatio(period={})", self.inner.period())
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Stochastic ==============================
|
||||
|
||||
#[pyclass(name = "IMI", module = "wickra._wickra", skip_from_py_object)]
|
||||
@@ -21124,6 +21250,245 @@ impl PyFibTimeZones {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== EWMA Volatility ==============================
|
||||
|
||||
#[pyclass(
|
||||
name = "EwmaVolatility",
|
||||
module = "wickra._wickra",
|
||||
skip_from_py_object
|
||||
)]
|
||||
#[derive(Clone)]
|
||||
struct PyEwmaVolatility {
|
||||
inner: wc::EwmaVolatility,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyEwmaVolatility {
|
||||
#[new]
|
||||
#[pyo3(signature = (lambda_=0.94))]
|
||||
fn new(lambda_: f64) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::EwmaVolatility::new(lambda_).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 lambda_(&self) -> f64 {
|
||||
self.inner.lambda()
|
||||
}
|
||||
#[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()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== GARCH(1,1) ==============================
|
||||
|
||||
#[pyclass(name = "Garch11", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyGarch11 {
|
||||
inner: wc::Garch11,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyGarch11 {
|
||||
#[new]
|
||||
#[pyo3(signature = (omega=0.000_002, alpha=0.1, beta=0.88))]
|
||||
fn new(omega: f64, alpha: f64, beta: f64) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::Garch11::new(omega, 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 params(&self) -> (f64, f64, f64) {
|
||||
self.inner.params()
|
||||
}
|
||||
#[getter]
|
||||
fn unconditional_variance(&self) -> f64 {
|
||||
self.inner.unconditional_variance()
|
||||
}
|
||||
#[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()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Volatility of Volatility ==============================
|
||||
|
||||
#[pyclass(
|
||||
name = "VolatilityOfVolatility",
|
||||
module = "wickra._wickra",
|
||||
skip_from_py_object
|
||||
)]
|
||||
#[derive(Clone)]
|
||||
struct PyVolatilityOfVolatility {
|
||||
inner: wc::VolatilityOfVolatility,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyVolatilityOfVolatility {
|
||||
#[new]
|
||||
#[pyo3(signature = (vol_window=20, vov_window=20))]
|
||||
fn new(vol_window: usize, vov_window: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::VolatilityOfVolatility::new(vol_window, vov_window).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 windows(&self) -> (usize, usize) {
|
||||
self.inner.windows()
|
||||
}
|
||||
#[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()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Volatility Cone ==============================
|
||||
|
||||
#[pyclass(
|
||||
name = "VolatilityCone",
|
||||
module = "wickra._wickra",
|
||||
skip_from_py_object
|
||||
)]
|
||||
#[derive(Clone)]
|
||||
struct PyVolatilityCone {
|
||||
inner: wc::VolatilityCone,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyVolatilityCone {
|
||||
#[new]
|
||||
#[pyo3(signature = (window=20, lookback=60))]
|
||||
fn new(window: usize, lookback: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::VolatilityCone::new(window, lookback).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult<Option<ConeBands>> {
|
||||
let c = extract_candle(candle)?;
|
||||
Ok(self
|
||||
.inner
|
||||
.update(c)
|
||||
.map(|o| (o.current, o.min, o.median, o.max, o.percentile)))
|
||||
}
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray2<f64>>> {
|
||||
let h = high
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let l = low
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let c = close
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
if h.len() != l.len() || l.len() != c.len() {
|
||||
return Err(PyValueError::new_err(
|
||||
"high, low, close must be equal length",
|
||||
));
|
||||
}
|
||||
let n = h.len();
|
||||
let mut out = vec![f64::NAN; n * 5];
|
||||
for i in 0..n {
|
||||
let candle = wc::Candle::new(c[i], h[i], l[i], c[i], 0.0, 0).map_err(map_err)?;
|
||||
if let Some(o) = self.inner.update(candle) {
|
||||
out[i * 5] = o.current;
|
||||
out[i * 5 + 1] = o.min;
|
||||
out[i * 5 + 2] = o.median;
|
||||
out[i * 5 + 3] = o.max;
|
||||
out[i * 5 + 4] = o.percentile;
|
||||
}
|
||||
}
|
||||
Ok(numpy::ndarray::Array2::from_shape_vec((n, 5), out)
|
||||
.expect("shape consistent")
|
||||
.into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn windows(&self) -> (usize, usize) {
|
||||
self.inner.windows()
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
#[pymodule]
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
@@ -21563,5 +21928,11 @@ fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_class::<PyTsfOscillator>()?;
|
||||
m.add_class::<PyMacdHistogram>()?;
|
||||
m.add_class::<PyPpoHistogram>()?;
|
||||
m.add_class::<PyBipowerVariation>()?;
|
||||
m.add_class::<PyVolatilityRatio>()?;
|
||||
m.add_class::<PyEwmaVolatility>()?;
|
||||
m.add_class::<PyGarch11>()?;
|
||||
m.add_class::<PyVolatilityOfVolatility>()?;
|
||||
m.add_class::<PyVolatilityCone>()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -45,6 +45,10 @@ def ohlcv() -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
|
||||
# --- Scalar (f64 -> f64) indicators ---------------------------------------
|
||||
|
||||
SCALAR = [
|
||||
(ta.BipowerVariation, (20,)),
|
||||
(ta.VolatilityOfVolatility, (20, 20)),
|
||||
(ta.Garch11, (0.000002, 0.1, 0.88)),
|
||||
(ta.EwmaVolatility, (0.94,)),
|
||||
(ta.PpoHistogram, (3, 6, 3)),
|
||||
(ta.MacdHistogram, (3, 6, 3)),
|
||||
(ta.TsfOscillator, (3,)),
|
||||
@@ -361,6 +365,7 @@ def test_relative_strength_streaming_matches_batch():
|
||||
# 6-tuple candle; the batch helper takes only the columns it needs.
|
||||
|
||||
CANDLE_SCALAR = {
|
||||
"VolatilityRatio": (lambda: ta.VolatilityRatio(14), lambda ind, h, l, c, v: ind.batch(h, l, c)),
|
||||
"TTM_TREND": (lambda: ta.TTM_TREND(6), lambda ind, h, l, c, v: ind.batch(h, l, c)),
|
||||
"StochasticCCI": (lambda: ta.StochasticCCI(14), lambda ind, h, l, c, v: ind.batch(h, l, c)),
|
||||
# Per-bar OHLC transforms (open matters). The streaming harness feeds
|
||||
@@ -899,6 +904,11 @@ def test_candle_scalar_streaming_matches_batch(name, ohlcv):
|
||||
# --- Candle-input, multi-output indicators --------------------------------
|
||||
|
||||
MULTI = {
|
||||
"VolatilityCone": (
|
||||
lambda: ta.VolatilityCone(20, 60),
|
||||
lambda ind, h, l, c, v: ind.batch(h, l, c),
|
||||
5,
|
||||
),
|
||||
"KasePermissionStochastic": (
|
||||
lambda: ta.KasePermissionStochastic(9, 3),
|
||||
lambda ind, h, l, c, v: ind.batch(h, l, c),
|
||||
@@ -2890,6 +2900,24 @@ def test_ppo_histogram_reference():
|
||||
assert t.update(100.0 + i * 2.0) is None
|
||||
assert t.update(100.0 + 7 * 2.0) == pytest.approx(-0.052098, abs=1e-6)
|
||||
|
||||
|
||||
def test_ewma_volatility_reference():
|
||||
t = ta.EwmaVolatility(0.94)
|
||||
assert t.update(100.0) is None
|
||||
assert t.update(110.0) == pytest.approx(0.09531017980432493)
|
||||
assert t.update(99.0) == pytest.approx(0.0959428936787596)
|
||||
|
||||
|
||||
def test_garch11_reference():
|
||||
t = ta.Garch11(0.000002, 0.1, 0.88)
|
||||
assert t.update(100.0) is None
|
||||
assert t.update(110.0) == pytest.approx(0.009999999999999995)
|
||||
assert t.update(99.0) == pytest.approx(0.031597516317477786)
|
||||
|
||||
|
||||
def test_volatility_cone_reference():
|
||||
t = ta.VolatilityCone(20, 60)
|
||||
|
||||
# --- Lifecycle ------------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
@@ -2476,6 +2476,44 @@ impl WasmKasePermissionStochastic {
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = VolatilityRatio)]
|
||||
pub struct WasmVolatilityRatio {
|
||||
inner: wc::VolatilityRatio,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = VolatilityRatio)]
|
||||
impl WasmVolatilityRatio {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(period: usize) -> Result<WasmVolatilityRatio, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::VolatilityRatio::new(period).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
pub fn update(&mut self, high: f64, low: f64, close: f64) -> Result<Option<f64>, JsError> {
|
||||
let c = make_candle(high, low, close, 0.0)?;
|
||||
Ok(self.inner.update(c))
|
||||
}
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
high: &[f64],
|
||||
low: &[f64],
|
||||
close: &[f64],
|
||||
) -> Result<Float64Array, JsError> {
|
||||
if high.len() != low.len() || low.len() != close.len() {
|
||||
return Err(JsError::new("high, low, close must be equal length"));
|
||||
}
|
||||
let mut out = Vec::with_capacity(high.len());
|
||||
for i in 0..high.len() {
|
||||
let c = make_candle(high[i], low[i], close[i], 0.0)?;
|
||||
out.push(self.inner.update(c).unwrap_or(f64::NAN));
|
||||
}
|
||||
Ok(Float64Array::from(out.as_slice()))
|
||||
}
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = Stochastic)]
|
||||
pub struct WasmStoch {
|
||||
inner: wc::Stochastic,
|
||||
@@ -10656,6 +10694,76 @@ wasm_scalar_indicator!(WasmTrendStrengthIndex, "TREND_STRENGTH_INDEX", wc::Trend
|
||||
wasm_scalar_indicator!(WasmTsfOscillator, "TsfOscillator", wc::TsfOscillator, period: usize);
|
||||
wasm_scalar_indicator!(WasmMacdHistogram, "MacdHistogram", wc::MacdHistogram, fast: usize, slow: usize, signal: usize);
|
||||
wasm_scalar_indicator!(WasmPpoHistogram, "PpoHistogram", wc::PpoHistogram, fast: usize, slow: usize, signal: usize);
|
||||
wasm_scalar_indicator!(WasmBipowerVariation, "BipowerVariation", wc::BipowerVariation, period: usize);
|
||||
wasm_scalar_indicator!(WasmEwmaVolatility, "EwmaVolatility", wc::EwmaVolatility, lambda: f64);
|
||||
wasm_scalar_indicator!(WasmGarch11, "Garch11", wc::Garch11, omega: f64, alpha: f64, beta: f64);
|
||||
wasm_scalar_indicator!(WasmVolatilityOfVolatility, "VolatilityOfVolatility", wc::VolatilityOfVolatility, vol_window: usize, vov_window: usize);
|
||||
|
||||
// --- VolatilityCone: Candle in, struct out (current/min/median/max/percentile) ---
|
||||
|
||||
#[wasm_bindgen(js_name = VolatilityCone)]
|
||||
pub struct WasmVolatilityCone {
|
||||
inner: wc::VolatilityCone,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = VolatilityCone)]
|
||||
impl WasmVolatilityCone {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(window: usize, lookback: usize) -> Result<WasmVolatilityCone, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::VolatilityCone::new(window, lookback).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
pub fn update(&mut self, high: f64, low: f64, close: f64) -> Result<JsValue, JsError> {
|
||||
let c = make_candle(high, low, close, 0.0)?;
|
||||
Ok(match self.inner.update(c) {
|
||||
Some(o) => {
|
||||
let obj = Object::new();
|
||||
Reflect::set(&obj, &"current".into(), &o.current.into()).ok();
|
||||
Reflect::set(&obj, &"min".into(), &o.min.into()).ok();
|
||||
Reflect::set(&obj, &"median".into(), &o.median.into()).ok();
|
||||
Reflect::set(&obj, &"max".into(), &o.max.into()).ok();
|
||||
Reflect::set(&obj, &"percentile".into(), &o.percentile.into()).ok();
|
||||
obj.into()
|
||||
}
|
||||
None => JsValue::NULL,
|
||||
})
|
||||
}
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
high: &[f64],
|
||||
low: &[f64],
|
||||
close: &[f64],
|
||||
) -> Result<Float64Array, JsError> {
|
||||
let n = high.len();
|
||||
if low.len() != n || close.len() != n {
|
||||
return Err(JsError::new("high, low, close must be equal length"));
|
||||
}
|
||||
let mut out = vec![f64::NAN; n * 5];
|
||||
for i in 0..n {
|
||||
let c = make_candle(high[i], low[i], close[i], 0.0)?;
|
||||
if let Some(o) = self.inner.update(c) {
|
||||
out[i * 5] = o.current;
|
||||
out[i * 5 + 1] = o.min;
|
||||
out[i * 5 + 2] = o.median;
|
||||
out[i * 5 + 3] = o.max;
|
||||
out[i * 5 + 4] = o.percentile;
|
||||
}
|
||||
}
|
||||
Ok(Float64Array::from(out.as_slice()))
|
||||
}
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
#[wasm_bindgen(js_name = isReady)]
|
||||
pub fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
#[wasm_bindgen(js_name = warmupPeriod)]
|
||||
pub fn warmup_period(&self) -> usize {
|
||||
self.inner.warmup_period()
|
||||
}
|
||||
}
|
||||
|
||||
// --- DrawdownDuration: u32 output, no constructor args ---
|
||||
|
||||
|
||||
@@ -0,0 +1,281 @@
|
||||
//! Realized Bipower Variation — a jump-robust quadratic-variation estimator.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Realized Bipower Variation — the sum of *adjacent* absolute log-return
|
||||
/// products over the trailing `period` returns, scaled to estimate integrated
|
||||
/// variance.
|
||||
///
|
||||
/// ```text
|
||||
/// r_t = ln(price_t / price_{t−1})
|
||||
/// BV = (π / 2) · Σ |r_t| · |r_{t−1}| over the window
|
||||
/// ```
|
||||
///
|
||||
/// Bipower variation (Barndorff-Nielsen & Shephard 2004) estimates the same
|
||||
/// integrated variance as [`RealizedVolatility`](crate::RealizedVolatility)'s
|
||||
/// `Σ r²`, but by multiplying *neighbouring* absolute returns rather than
|
||||
/// squaring a single one. A price jump inflates exactly one return; because that
|
||||
/// return appears in a product with its (ordinary) neighbour rather than squared,
|
||||
/// its contribution stays bounded — so `BV` is **robust to jumps** while realized
|
||||
/// variance is not. The constant `π / 2 = μ₁⁻²` (with `μ₁ = E|Z| = √(2/π)` for a
|
||||
/// standard normal) debiases the product of two half-normal magnitudes back to a
|
||||
/// variance scale.
|
||||
///
|
||||
/// The output is on the **variance** scale (the jump-robust counterpart of
|
||||
/// realized *variance*, not volatility); take its square root for a volatility,
|
||||
/// and compare `RV − BV` to isolate the jump contribution. A window of `period`
|
||||
/// returns contributes `period − 1` adjacent products; each `update` is O(1) via
|
||||
/// a running sum.
|
||||
///
|
||||
/// Non-finite and non-positive prices are ignored (the log return would be
|
||||
/// undefined): the tick is dropped, state is left untouched, and the last value
|
||||
/// is returned.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{BipowerVariation, Indicator};
|
||||
///
|
||||
/// let mut indicator = BipowerVariation::new(20).unwrap();
|
||||
/// let mut last = None;
|
||||
/// for i in 0..80 {
|
||||
/// last = indicator.update(100.0 + (f64::from(i) * 0.3).sin() * 5.0);
|
||||
/// }
|
||||
/// assert!(last.is_some());
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BipowerVariation {
|
||||
period: usize,
|
||||
prev_price: Option<f64>,
|
||||
/// Rolling window of the last `period` log returns.
|
||||
window: VecDeque<f64>,
|
||||
/// Running sum of adjacent absolute-return products inside the window.
|
||||
sum_adjacent: f64,
|
||||
last: Option<f64>,
|
||||
}
|
||||
|
||||
impl BipowerVariation {
|
||||
/// Construct a new bipower-variation indicator.
|
||||
///
|
||||
/// `period` is the number of log returns in the rolling window; the estimate
|
||||
/// uses the `period − 1` adjacent products between them.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns [`Error::PeriodZero`] if `period == 0`, or
|
||||
/// [`Error::InvalidPeriod`] if `period == 1` (an adjacent product needs at
|
||||
/// least two returns).
|
||||
pub fn new(period: usize) -> Result<Self> {
|
||||
if period == 0 {
|
||||
return Err(Error::PeriodZero);
|
||||
}
|
||||
if period < 2 {
|
||||
return Err(Error::InvalidPeriod {
|
||||
message: "bipower variation period must be >= 2",
|
||||
});
|
||||
}
|
||||
Ok(Self {
|
||||
period,
|
||||
prev_price: None,
|
||||
window: VecDeque::with_capacity(period),
|
||||
sum_adjacent: 0.0,
|
||||
last: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured period.
|
||||
pub const fn period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
}
|
||||
|
||||
/// `μ₁⁻² = π / 2`, the debiasing constant for a product of half-normal returns.
|
||||
const MU1_INV_SQ: f64 = std::f64::consts::FRAC_PI_2;
|
||||
|
||||
impl Indicator for BipowerVariation {
|
||||
type Input = f64;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, input: f64) -> Option<f64> {
|
||||
// Non-finite / non-positive prices are skipped: `ln(input / prev)` is
|
||||
// undefined, so the tick must not enter the return window.
|
||||
if !input.is_finite() || input <= 0.0 {
|
||||
return self.last;
|
||||
}
|
||||
let Some(prev) = self.prev_price else {
|
||||
self.prev_price = Some(input);
|
||||
return None;
|
||||
};
|
||||
self.prev_price = Some(input);
|
||||
// `prev` came from `self.prev_price`, gated by the guard above, so it is
|
||||
// finite and positive — the log return is always well-defined.
|
||||
let r = (input / prev).ln();
|
||||
// The incoming return forms a product with the current last return.
|
||||
if let Some(&back) = self.window.back() {
|
||||
self.sum_adjacent += back.abs() * r.abs();
|
||||
}
|
||||
self.window.push_back(r);
|
||||
if self.window.len() > self.period {
|
||||
let first = self.window.pop_front().expect("window is non-empty");
|
||||
// The product between the dropped return and the new front leaves.
|
||||
let second = *self.window.front().expect("window still has >= 1 element");
|
||||
self.sum_adjacent -= first.abs() * second.abs();
|
||||
}
|
||||
if self.window.len() < self.period {
|
||||
return None;
|
||||
}
|
||||
// Products are non-negative; the rolling subtraction can leave a tiny
|
||||
// negative residual when returns are ~0, so clamp before scaling.
|
||||
let bv = MU1_INV_SQ * self.sum_adjacent.max(0.0);
|
||||
self.last = Some(bv);
|
||||
Some(bv)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.prev_price = None;
|
||||
self.window.clear();
|
||||
self.sum_adjacent = 0.0;
|
||||
self.last = None;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
// The first log return needs a previous price, then the window fills.
|
||||
self.period + 1
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.last.is_some()
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"BipowerVariation"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
#[test]
|
||||
fn rejects_zero_period() {
|
||||
assert!(matches!(BipowerVariation::new(0), Err(Error::PeriodZero)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_period_one() {
|
||||
assert!(matches!(
|
||||
BipowerVariation::new(1),
|
||||
Err(Error::InvalidPeriod { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let bv = BipowerVariation::new(20).unwrap();
|
||||
assert_eq!(bv.period(), 20);
|
||||
assert_eq!(bv.warmup_period(), 21);
|
||||
assert_eq!(bv.name(), "BipowerVariation");
|
||||
assert!(!bv.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_emission_at_warmup_period() {
|
||||
let mut bv = BipowerVariation::new(5).unwrap();
|
||||
let out = bv.batch(&(1..=20).map(f64::from).collect::<Vec<_>>());
|
||||
for v in out.iter().take(5) {
|
||||
assert!(v.is_none());
|
||||
}
|
||||
assert!(out[5].is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn known_value() {
|
||||
// period = 2: one adjacent product. r1 = ln(1.1), r2 = ln(0.9).
|
||||
// BV = (π/2)·|r1|·|r2|.
|
||||
let mut bv = BipowerVariation::new(2).unwrap();
|
||||
let out = bv.batch(&[100.0, 110.0, 99.0]);
|
||||
assert!(out[1].is_none());
|
||||
let r1 = (110.0_f64 / 100.0).ln();
|
||||
let r2 = (99.0_f64 / 110.0).ln();
|
||||
let expected = std::f64::consts::FRAC_PI_2 * r1.abs() * r2.abs();
|
||||
assert_relative_eq!(out[2].unwrap(), expected, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rolling_window_drops_oldest_product() {
|
||||
// period = 2, four prices -> two emissions, each a single product.
|
||||
let mut bv = BipowerVariation::new(2).unwrap();
|
||||
let out = bv.batch(&[100.0, 110.0, 99.0, 105.0]);
|
||||
let r2 = (99.0_f64 / 110.0).ln();
|
||||
let r3 = (105.0_f64 / 99.0).ln();
|
||||
let expected = std::f64::consts::FRAC_PI_2 * r2.abs() * r3.abs();
|
||||
assert_relative_eq!(out[3].unwrap(), expected, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn constant_series_yields_zero() {
|
||||
let mut bv = BipowerVariation::new(10).unwrap();
|
||||
for v in bv.batch(&[100.0; 40]).into_iter().flatten() {
|
||||
assert_relative_eq!(v, 0.0, epsilon = 1e-12);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn output_is_non_negative() {
|
||||
let mut bv = BipowerVariation::new(20).unwrap();
|
||||
let prices: Vec<f64> = (1..=200)
|
||||
.map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 12.0)
|
||||
.collect();
|
||||
for v in bv.batch(&prices).into_iter().flatten() {
|
||||
assert!(v >= 0.0, "bipower variation must be non-negative, got {v}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_non_finite_input() {
|
||||
let mut bv = BipowerVariation::new(5).unwrap();
|
||||
let out = bv.batch(&(1..=20).map(f64::from).collect::<Vec<_>>());
|
||||
let last = *out.last().unwrap();
|
||||
assert!(last.is_some());
|
||||
assert_eq!(bv.update(f64::NAN), last);
|
||||
assert_eq!(bv.update(f64::INFINITY), last);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_non_positive_prices() {
|
||||
let mut bv = BipowerVariation::new(5).unwrap();
|
||||
let warmup = bv.batch(&(1..=20).map(f64::from).collect::<Vec<_>>());
|
||||
let baseline = warmup.last().copied().flatten().expect("warmed up");
|
||||
assert_eq!(bv.update(-5.0), Some(baseline));
|
||||
assert_eq!(bv.update(0.0), Some(baseline));
|
||||
// State untouched: a clone advanced by the same real tick agrees.
|
||||
let mut control = bv.clone();
|
||||
let after = bv.update(21.0).expect("ready");
|
||||
assert_eq!(control.update(21.0).expect("ready"), after);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut bv = BipowerVariation::new(5).unwrap();
|
||||
bv.batch(&(1..=20).map(f64::from).collect::<Vec<_>>());
|
||||
assert!(bv.is_ready());
|
||||
bv.reset();
|
||||
assert!(!bv.is_ready());
|
||||
assert_eq!(bv.update(1.0), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let prices: Vec<f64> = (1..=120)
|
||||
.map(|i| 100.0 + (f64::from(i) * 0.25).sin() * 9.0)
|
||||
.collect();
|
||||
let batch = BipowerVariation::new(20).unwrap().batch(&prices);
|
||||
let mut b = BipowerVariation::new(20).unwrap();
|
||||
let streamed: Vec<_> = prices.iter().map(|p| b.update(*p)).collect();
|
||||
assert_eq!(batch, streamed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
//! EWMA Volatility — `RiskMetrics` exponentially-weighted volatility.
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// EWMA Volatility — the `RiskMetrics` exponentially-weighted estimate of the
|
||||
/// volatility of log returns.
|
||||
///
|
||||
/// ```text
|
||||
/// r_t = ln(price_t / price_{t−1})
|
||||
/// σ²_t = λ · σ²_{t−1} + (1 − λ) · r²_t
|
||||
/// EWMA = √σ²_t
|
||||
/// ```
|
||||
///
|
||||
/// Unlike [`HistoricalVolatility`](crate::HistoricalVolatility) — an equally
|
||||
/// weighted, mean-centred sample standard deviation over a fixed window — the
|
||||
/// EWMA estimator weights recent squared returns geometrically by the decay
|
||||
/// factor `λ`. The most recent return carries weight `1 − λ`, the one before it
|
||||
/// `λ(1 − λ)`, and so on, so the estimate reacts to a volatility shock
|
||||
/// immediately and then forgets it at rate `λ`. This is the J.P. Morgan
|
||||
/// `RiskMetrics` one-parameter model; the standard daily decay is `λ = 0.94`
|
||||
/// (monthly `0.97`). No mean is subtracted: squared returns *are* the variance
|
||||
/// contribution, which matches the `RiskMetrics` assumption of a zero conditional
|
||||
/// mean over short horizons.
|
||||
///
|
||||
/// The recursion is seeded with the first squared return (`σ²₁ = r²₁`) and emits
|
||||
/// from the first return onward, so the very first reading is a one-observation
|
||||
/// estimate that the decay then refines. Each `update` is O(1).
|
||||
///
|
||||
/// Non-finite and non-positive prices are ignored (the log return would be
|
||||
/// undefined): the tick is dropped, state is left untouched, and the last value
|
||||
/// is returned.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{EwmaVolatility, Indicator};
|
||||
///
|
||||
/// let mut indicator = EwmaVolatility::new(0.94).unwrap();
|
||||
/// let mut last = None;
|
||||
/// for i in 0..80 {
|
||||
/// last = indicator.update(100.0 + (f64::from(i) * 0.3).sin() * 5.0);
|
||||
/// }
|
||||
/// assert!(last.is_some());
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EwmaVolatility {
|
||||
lambda: f64,
|
||||
prev_price: Option<f64>,
|
||||
/// Exponentially-weighted variance of log returns; `None` until seeded.
|
||||
variance: Option<f64>,
|
||||
last: Option<f64>,
|
||||
}
|
||||
|
||||
impl EwmaVolatility {
|
||||
/// Construct a new EWMA-volatility indicator.
|
||||
///
|
||||
/// `lambda` is the decay factor, strictly between `0` and `1` (`RiskMetrics`
|
||||
/// uses `0.94` for daily data). Larger `lambda` means a longer memory and a
|
||||
/// smoother estimate.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns [`Error::InvalidParameter`] if `lambda` is not finite or not in
|
||||
/// the open interval `(0, 1)`.
|
||||
pub fn new(lambda: f64) -> Result<Self> {
|
||||
if !lambda.is_finite() || lambda <= 0.0 || lambda >= 1.0 {
|
||||
return Err(Error::InvalidParameter {
|
||||
message: "EWMA volatility lambda must be in the open interval (0, 1)",
|
||||
});
|
||||
}
|
||||
Ok(Self {
|
||||
lambda,
|
||||
prev_price: None,
|
||||
variance: None,
|
||||
last: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured decay factor.
|
||||
pub const fn lambda(&self) -> f64 {
|
||||
self.lambda
|
||||
}
|
||||
|
||||
/// Current value if available.
|
||||
pub const fn value(&self) -> Option<f64> {
|
||||
self.last
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for EwmaVolatility {
|
||||
type Input = f64;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, input: f64) -> Option<f64> {
|
||||
// Non-finite / non-positive prices are skipped: `ln(input / prev)` is
|
||||
// undefined, so the tick must not enter the variance recursion.
|
||||
if !input.is_finite() || input <= 0.0 {
|
||||
return self.last;
|
||||
}
|
||||
let Some(prev) = self.prev_price else {
|
||||
self.prev_price = Some(input);
|
||||
return None;
|
||||
};
|
||||
self.prev_price = Some(input);
|
||||
// `prev` came from `self.prev_price`, gated by the guard above, so it is
|
||||
// finite and positive — the log return is always well-defined.
|
||||
let r = (input / prev).ln();
|
||||
let var = match self.variance {
|
||||
// Seed the recursion with the first squared return.
|
||||
None => r * r,
|
||||
Some(prev_var) => self.lambda * prev_var + (1.0 - self.lambda) * r * r,
|
||||
};
|
||||
self.variance = Some(var);
|
||||
// `var` is a convex combination of non-negative terms, but rounding can
|
||||
// leave a tiny negative residual when every return is ~0; clamp first.
|
||||
let vol = var.max(0.0).sqrt();
|
||||
self.last = Some(vol);
|
||||
Some(vol)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.prev_price = None;
|
||||
self.variance = None;
|
||||
self.last = None;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
// The first log return needs a previous price; the estimate is seeded
|
||||
// and emitted on that first return.
|
||||
2
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.last.is_some()
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"EwmaVolatility"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
#[test]
|
||||
fn rejects_invalid_lambda() {
|
||||
for bad in [0.0, 1.0, -0.5, 1.5, f64::NAN, f64::INFINITY] {
|
||||
assert!(matches!(
|
||||
EwmaVolatility::new(bad),
|
||||
Err(Error::InvalidParameter { .. })
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let ewma = EwmaVolatility::new(0.94).unwrap();
|
||||
assert_relative_eq!(ewma.lambda(), 0.94);
|
||||
assert_eq!(ewma.warmup_period(), 2);
|
||||
assert_eq!(ewma.name(), "EwmaVolatility");
|
||||
assert!(!ewma.is_ready());
|
||||
assert_eq!(ewma.value(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_emission_at_warmup_period() {
|
||||
let mut ewma = EwmaVolatility::new(0.94).unwrap();
|
||||
assert_eq!(ewma.update(100.0), None);
|
||||
let out = ewma.update(110.0);
|
||||
assert!(out.is_some());
|
||||
assert!(ewma.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn known_value() {
|
||||
// r1 = ln(110/100), r2 = ln(99/110). Seed σ²₁ = r1²; then
|
||||
// σ²₂ = λ·r1² + (1−λ)·r2².
|
||||
let lambda = 0.94;
|
||||
let mut ewma = EwmaVolatility::new(lambda).unwrap();
|
||||
let out = ewma.batch(&[100.0, 110.0, 99.0]);
|
||||
let r1 = (110.0_f64 / 100.0).ln();
|
||||
let r2 = (99.0_f64 / 110.0).ln();
|
||||
assert_relative_eq!(out[1].unwrap(), r1.abs(), epsilon = 1e-12);
|
||||
let var2 = lambda * r1 * r1 + (1.0 - lambda) * r2 * r2;
|
||||
assert_relative_eq!(out[2].unwrap(), var2.sqrt(), epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn constant_series_yields_zero() {
|
||||
let mut ewma = EwmaVolatility::new(0.9).unwrap();
|
||||
for v in ewma.batch(&[100.0; 40]).into_iter().flatten() {
|
||||
assert_relative_eq!(v, 0.0, epsilon = 1e-12);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn output_is_non_negative() {
|
||||
let mut ewma = EwmaVolatility::new(0.94).unwrap();
|
||||
let prices: Vec<f64> = (1..=200)
|
||||
.map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 12.0)
|
||||
.collect();
|
||||
for v in ewma.batch(&prices).into_iter().flatten() {
|
||||
assert!(v >= 0.0, "EWMA volatility must be non-negative, got {v}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_non_finite_input() {
|
||||
let mut ewma = EwmaVolatility::new(0.94).unwrap();
|
||||
let out = ewma.batch(&(1..=20).map(f64::from).collect::<Vec<_>>());
|
||||
let last = *out.last().unwrap();
|
||||
assert!(last.is_some());
|
||||
assert_eq!(ewma.update(f64::NAN), last);
|
||||
assert_eq!(ewma.update(f64::INFINITY), last);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_non_positive_prices() {
|
||||
let mut ewma = EwmaVolatility::new(0.94).unwrap();
|
||||
let warmup = ewma.batch(&(1..=20).map(f64::from).collect::<Vec<_>>());
|
||||
let baseline = warmup.last().copied().flatten().expect("warmed up");
|
||||
assert_eq!(ewma.update(-5.0), Some(baseline));
|
||||
assert_eq!(ewma.update(0.0), Some(baseline));
|
||||
// State untouched: a clone advanced by the same real tick agrees.
|
||||
let mut control = ewma.clone();
|
||||
let after = ewma.update(21.0).expect("ready");
|
||||
assert_eq!(control.update(21.0).expect("ready"), after);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_non_positive_before_first_price() {
|
||||
// The skip guard fires before any previous price exists.
|
||||
let mut ewma = EwmaVolatility::new(0.94).unwrap();
|
||||
assert_eq!(ewma.update(0.0), None);
|
||||
assert_eq!(ewma.update(f64::NAN), None);
|
||||
assert_eq!(ewma.update(100.0), None);
|
||||
assert!(ewma.update(110.0).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut ewma = EwmaVolatility::new(0.94).unwrap();
|
||||
ewma.batch(&(1..=20).map(f64::from).collect::<Vec<_>>());
|
||||
assert!(ewma.is_ready());
|
||||
ewma.reset();
|
||||
assert!(!ewma.is_ready());
|
||||
assert_eq!(ewma.value(), None);
|
||||
assert_eq!(ewma.update(1.0), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let prices: Vec<f64> = (1..=120)
|
||||
.map(|i| 100.0 + (f64::from(i) * 0.25).sin() * 9.0)
|
||||
.collect();
|
||||
let batch = EwmaVolatility::new(0.94).unwrap().batch(&prices);
|
||||
let mut b = EwmaVolatility::new(0.94).unwrap();
|
||||
let streamed: Vec<_> = prices.iter().map(|p| b.update(*p)).collect();
|
||||
assert_eq!(batch, streamed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
//! GARCH(1,1) — conditional volatility with a long-run-variance anchor.
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// GARCH(1,1) conditional volatility — the square root of the
|
||||
/// generalized-autoregressive-conditional-heteroskedasticity variance recursion.
|
||||
///
|
||||
/// ```text
|
||||
/// r_t = ln(price_t / price_{t−1})
|
||||
/// σ²_t = ω + α · r²_{t−1} + β · σ²_{t−1}
|
||||
/// out = √σ²_t
|
||||
/// ```
|
||||
///
|
||||
/// GARCH(1,1) (Bollerslev 1986) generalizes the
|
||||
/// [`EwmaVolatility`](crate::EwmaVolatility) recursion by adding a constant `ω`,
|
||||
/// which pins the process to a finite long-run (unconditional) variance
|
||||
/// `ω / (1 − α − β)`. The `α` term gives weight to the latest squared return
|
||||
/// (the "ARCH" shock) and `β` to the previous variance (the "GARCH"
|
||||
/// persistence). When `ω = 0` and `α + β = 1` the model degenerates to EWMA; a
|
||||
/// proper GARCH keeps `ω > 0` and `α + β < 1` so volatility mean-reverts rather
|
||||
/// than drifting.
|
||||
///
|
||||
/// The recursion is seeded with the unconditional variance (`σ²₁ = ω / (1 − α −
|
||||
/// β)`) and emits from the first log return onward. Unlike EWMA — which decays to
|
||||
/// zero on a flat series — a flat series here mean-reverts toward `ω / (1 − β)`
|
||||
/// (the `α`-term vanishes but the `ω` floor and the `β` carry remain), so the
|
||||
/// output is always strictly positive. Each `update` is O(1).
|
||||
///
|
||||
/// Non-finite and non-positive prices are ignored (the log return would be
|
||||
/// undefined): the tick is dropped, state is left untouched, and the last value
|
||||
/// is returned.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Garch11, Indicator};
|
||||
///
|
||||
/// // Typical equity daily estimate.
|
||||
/// let mut indicator = Garch11::new(0.000_002, 0.10, 0.88).unwrap();
|
||||
/// let mut last = None;
|
||||
/// for i in 0..80 {
|
||||
/// last = indicator.update(100.0 + (f64::from(i) * 0.3).sin() * 5.0);
|
||||
/// }
|
||||
/// assert!(last.is_some());
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Garch11 {
|
||||
omega: f64,
|
||||
alpha: f64,
|
||||
beta: f64,
|
||||
unconditional: f64,
|
||||
prev_price: Option<f64>,
|
||||
/// `(σ²_{t−1}, r²_{t−1})` — previous variance and previous squared return.
|
||||
state: Option<(f64, f64)>,
|
||||
last: Option<f64>,
|
||||
}
|
||||
|
||||
impl Garch11 {
|
||||
/// Construct a new GARCH(1,1) indicator from its three parameters.
|
||||
///
|
||||
/// `omega` (`ω`) is the constant variance floor, `alpha` (`α`) the weight on
|
||||
/// the latest squared return, and `beta` (`β`) the persistence of the
|
||||
/// previous variance.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns [`Error::InvalidParameter`] unless every parameter is finite,
|
||||
/// `omega > 0`, `alpha >= 0`, `beta >= 0`, and `alpha + beta < 1` (the
|
||||
/// covariance-stationarity condition that gives a finite long-run variance).
|
||||
pub fn new(omega: f64, alpha: f64, beta: f64) -> Result<Self> {
|
||||
if !omega.is_finite() || !alpha.is_finite() || !beta.is_finite() {
|
||||
return Err(Error::InvalidParameter {
|
||||
message: "GARCH(1,1) parameters must be finite",
|
||||
});
|
||||
}
|
||||
if omega <= 0.0 {
|
||||
return Err(Error::InvalidParameter {
|
||||
message: "GARCH(1,1) omega must be > 0",
|
||||
});
|
||||
}
|
||||
if alpha < 0.0 || beta < 0.0 {
|
||||
return Err(Error::InvalidParameter {
|
||||
message: "GARCH(1,1) alpha and beta must be >= 0",
|
||||
});
|
||||
}
|
||||
if alpha + beta >= 1.0 {
|
||||
return Err(Error::InvalidParameter {
|
||||
message: "GARCH(1,1) requires alpha + beta < 1 (covariance stationarity)",
|
||||
});
|
||||
}
|
||||
Ok(Self {
|
||||
omega,
|
||||
alpha,
|
||||
beta,
|
||||
unconditional: omega / (1.0 - alpha - beta),
|
||||
prev_price: None,
|
||||
state: None,
|
||||
last: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured `(omega, alpha, beta)`.
|
||||
pub const fn params(&self) -> (f64, f64, f64) {
|
||||
(self.omega, self.alpha, self.beta)
|
||||
}
|
||||
|
||||
/// Long-run (unconditional) variance `ω / (1 − α − β)`.
|
||||
pub const fn unconditional_variance(&self) -> f64 {
|
||||
self.unconditional
|
||||
}
|
||||
|
||||
/// Current value if available.
|
||||
pub const fn value(&self) -> Option<f64> {
|
||||
self.last
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for Garch11 {
|
||||
type Input = f64;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, input: f64) -> Option<f64> {
|
||||
// Non-finite / non-positive prices are skipped: `ln(input / prev)` is
|
||||
// undefined, so the tick must not enter the variance recursion.
|
||||
if !input.is_finite() || input <= 0.0 {
|
||||
return self.last;
|
||||
}
|
||||
let Some(prev) = self.prev_price else {
|
||||
self.prev_price = Some(input);
|
||||
return None;
|
||||
};
|
||||
self.prev_price = Some(input);
|
||||
// `prev` came from `self.prev_price`, gated by the guard above, so it is
|
||||
// finite and positive — the log return is always well-defined.
|
||||
let r = (input / prev).ln();
|
||||
let r_sq = r * r;
|
||||
let var = match self.state {
|
||||
// Seed the recursion with the unconditional variance.
|
||||
None => self.unconditional,
|
||||
Some((prev_var, prev_r_sq)) => {
|
||||
self.omega + self.alpha * prev_r_sq + self.beta * prev_var
|
||||
}
|
||||
};
|
||||
self.state = Some((var, r_sq));
|
||||
// `var` is `omega (> 0) + non-negative terms`, so it is strictly
|
||||
// positive — the square root is always well-defined.
|
||||
let vol = var.sqrt();
|
||||
self.last = Some(vol);
|
||||
Some(vol)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.prev_price = None;
|
||||
self.state = None;
|
||||
self.last = None;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
// The first log return needs a previous price; the estimate is seeded
|
||||
// with the unconditional variance and emitted on that first return.
|
||||
2
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.last.is_some()
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"Garch11"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
#[test]
|
||||
fn rejects_invalid_params() {
|
||||
assert!(matches!(
|
||||
Garch11::new(0.0, 0.1, 0.8),
|
||||
Err(Error::InvalidParameter { .. })
|
||||
));
|
||||
assert!(matches!(
|
||||
Garch11::new(-1.0, 0.1, 0.8),
|
||||
Err(Error::InvalidParameter { .. })
|
||||
));
|
||||
assert!(matches!(
|
||||
Garch11::new(0.001, -0.1, 0.8),
|
||||
Err(Error::InvalidParameter { .. })
|
||||
));
|
||||
assert!(matches!(
|
||||
Garch11::new(0.001, 0.1, -0.8),
|
||||
Err(Error::InvalidParameter { .. })
|
||||
));
|
||||
assert!(matches!(
|
||||
Garch11::new(0.001, 0.5, 0.5),
|
||||
Err(Error::InvalidParameter { .. })
|
||||
));
|
||||
assert!(matches!(
|
||||
Garch11::new(f64::NAN, 0.1, 0.8),
|
||||
Err(Error::InvalidParameter { .. })
|
||||
));
|
||||
assert!(matches!(
|
||||
Garch11::new(0.001, f64::INFINITY, 0.8),
|
||||
Err(Error::InvalidParameter { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let g = Garch11::new(0.001, 0.1, 0.85).unwrap();
|
||||
assert_eq!(g.params(), (0.001, 0.1, 0.85));
|
||||
assert_relative_eq!(g.unconditional_variance(), 0.001 / 0.05, epsilon = 1e-12);
|
||||
assert_eq!(g.warmup_period(), 2);
|
||||
assert_eq!(g.name(), "Garch11");
|
||||
assert!(!g.is_ready());
|
||||
assert_eq!(g.value(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_emission_is_unconditional() {
|
||||
// The first log return emits the seed = sqrt(unconditional variance),
|
||||
// independent of the return value.
|
||||
let g = Garch11::new(0.002, 0.1, 0.85);
|
||||
let mut g = g.unwrap();
|
||||
assert_eq!(g.update(100.0), None);
|
||||
let out = g.update(110.0).unwrap();
|
||||
assert_relative_eq!(out, (0.002_f64 / 0.05).sqrt(), epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn known_value() {
|
||||
// σ²₁ = uncond; σ²₂ = ω + α·r1² + β·uncond.
|
||||
let (omega, alpha, beta) = (0.002, 0.1, 0.85);
|
||||
let mut g = Garch11::new(omega, alpha, beta).unwrap();
|
||||
let out = g.batch(&[100.0, 110.0, 99.0]);
|
||||
let uncond = omega / (1.0 - alpha - beta);
|
||||
let r1 = (110.0_f64 / 100.0).ln();
|
||||
assert_relative_eq!(out[1].unwrap(), uncond.sqrt(), epsilon = 1e-12);
|
||||
let var2 = omega + alpha * r1 * r1 + beta * uncond;
|
||||
assert_relative_eq!(out[2].unwrap(), var2.sqrt(), epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flat_series_converges_to_long_run() {
|
||||
// With zero returns the alpha term vanishes; the variance mean-reverts
|
||||
// to the fixed point ω / (1 − β), NOT to zero (the key GARCH/EWMA
|
||||
// distinction).
|
||||
let (omega, beta) = (0.002, 0.85);
|
||||
let mut g = Garch11::new(omega, 0.10, beta).unwrap();
|
||||
let out = g.batch(&[100.0; 400]);
|
||||
let fixed_point = (omega / (1.0 - beta)).sqrt();
|
||||
assert_relative_eq!(out.last().unwrap().unwrap(), fixed_point, epsilon = 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn output_is_strictly_positive() {
|
||||
let mut g = Garch11::new(0.000_002, 0.1, 0.88).unwrap();
|
||||
let prices: Vec<f64> = (1..=200)
|
||||
.map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 12.0)
|
||||
.collect();
|
||||
for v in g.batch(&prices).into_iter().flatten() {
|
||||
assert!(
|
||||
v > 0.0,
|
||||
"GARCH volatility must be strictly positive, got {v}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_non_finite_input() {
|
||||
let mut g = Garch11::new(0.001, 0.1, 0.85).unwrap();
|
||||
let out = g.batch(&(1..=20).map(f64::from).collect::<Vec<_>>());
|
||||
let last = *out.last().unwrap();
|
||||
assert!(last.is_some());
|
||||
assert_eq!(g.update(f64::NAN), last);
|
||||
assert_eq!(g.update(f64::INFINITY), last);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_non_positive_prices() {
|
||||
let mut g = Garch11::new(0.001, 0.1, 0.85).unwrap();
|
||||
let warmup = g.batch(&(1..=20).map(f64::from).collect::<Vec<_>>());
|
||||
let baseline = warmup.last().copied().flatten().expect("warmed up");
|
||||
assert_eq!(g.update(-5.0), Some(baseline));
|
||||
assert_eq!(g.update(0.0), Some(baseline));
|
||||
// State untouched: a clone advanced by the same real tick agrees.
|
||||
let mut control = g.clone();
|
||||
let after = g.update(21.0).expect("ready");
|
||||
assert_eq!(control.update(21.0).expect("ready"), after);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_non_positive_before_first_price() {
|
||||
let mut g = Garch11::new(0.001, 0.1, 0.85).unwrap();
|
||||
assert_eq!(g.update(0.0), None);
|
||||
assert_eq!(g.update(f64::NAN), None);
|
||||
assert_eq!(g.update(100.0), None);
|
||||
assert!(g.update(110.0).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut g = Garch11::new(0.001, 0.1, 0.85).unwrap();
|
||||
g.batch(&(1..=20).map(f64::from).collect::<Vec<_>>());
|
||||
assert!(g.is_ready());
|
||||
g.reset();
|
||||
assert!(!g.is_ready());
|
||||
assert_eq!(g.value(), None);
|
||||
assert_eq!(g.update(1.0), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let prices: Vec<f64> = (1..=120)
|
||||
.map(|i| 100.0 + (f64::from(i) * 0.25).sin() * 9.0)
|
||||
.collect();
|
||||
let batch = Garch11::new(0.000_002, 0.1, 0.88).unwrap().batch(&prices);
|
||||
let mut b = Garch11::new(0.000_002, 0.1, 0.88).unwrap();
|
||||
let streamed: Vec<_> = prices.iter().map(|p| b.update(*p)).collect();
|
||||
assert_eq!(batch, streamed);
|
||||
}
|
||||
}
|
||||
@@ -48,6 +48,7 @@ mod bat;
|
||||
mod belt_hold;
|
||||
mod beta;
|
||||
mod beta_neutral_spread;
|
||||
mod bipower_variation;
|
||||
mod body_size_pct;
|
||||
mod bollinger;
|
||||
mod bollinger_bandwidth;
|
||||
@@ -118,6 +119,7 @@ mod empirical_mode_decomposition;
|
||||
mod engulfing;
|
||||
mod evening_doji_star;
|
||||
mod evwma;
|
||||
mod ewma_volatility;
|
||||
mod expectancy;
|
||||
mod falling_three_methods;
|
||||
mod fama;
|
||||
@@ -143,6 +145,7 @@ mod funding_rate_mean;
|
||||
mod funding_rate_zscore;
|
||||
mod gain_loss_ratio;
|
||||
mod gap_side_by_side_white;
|
||||
mod garch11;
|
||||
mod garman_klass;
|
||||
mod gartley;
|
||||
mod gator_oscillator;
|
||||
@@ -404,6 +407,9 @@ mod variance;
|
||||
mod variance_ratio;
|
||||
mod vertical_horizontal_filter;
|
||||
mod vidya;
|
||||
mod volatility_cone;
|
||||
mod volatility_of_volatility;
|
||||
mod volatility_ratio;
|
||||
mod volty_stop;
|
||||
mod volume_by_time_profile;
|
||||
mod volume_oscillator;
|
||||
@@ -471,6 +477,7 @@ pub use bat::Bat;
|
||||
pub use belt_hold::BeltHold;
|
||||
pub use beta::Beta;
|
||||
pub use beta_neutral_spread::BetaNeutralSpread;
|
||||
pub use bipower_variation::BipowerVariation;
|
||||
pub use body_size_pct::BodySizePct;
|
||||
pub use bollinger::{BollingerBands, BollingerOutput};
|
||||
pub use bollinger_bandwidth::BollingerBandwidth;
|
||||
@@ -541,6 +548,7 @@ pub use empirical_mode_decomposition::EmpiricalModeDecomposition;
|
||||
pub use engulfing::Engulfing;
|
||||
pub use evening_doji_star::EveningDojiStar;
|
||||
pub use evwma::Evwma;
|
||||
pub use ewma_volatility::EwmaVolatility;
|
||||
pub use expectancy::Expectancy;
|
||||
pub use falling_three_methods::FallingThreeMethods;
|
||||
pub use fama::Fama;
|
||||
@@ -566,6 +574,7 @@ pub use funding_rate_mean::FundingRateMean;
|
||||
pub use funding_rate_zscore::FundingRateZScore;
|
||||
pub use gain_loss_ratio::GainLossRatio;
|
||||
pub use gap_side_by_side_white::GapSideBySideWhite;
|
||||
pub use garch11::Garch11;
|
||||
pub use garman_klass::GarmanKlassVolatility;
|
||||
pub use gartley::Gartley;
|
||||
pub use gator_oscillator::{GatorOscillator, GatorOscillatorOutput};
|
||||
@@ -827,6 +836,9 @@ pub use variance::Variance;
|
||||
pub use variance_ratio::VarianceRatio;
|
||||
pub use vertical_horizontal_filter::VerticalHorizontalFilter;
|
||||
pub use vidya::Vidya;
|
||||
pub use volatility_cone::{VolatilityCone, VolatilityConeOutput};
|
||||
pub use volatility_of_volatility::VolatilityOfVolatility;
|
||||
pub use volatility_ratio::VolatilityRatio;
|
||||
pub use volty_stop::VoltyStop;
|
||||
pub use volume_by_time_profile::{VolumeByTimeProfile, VolumeByTimeProfileOutput};
|
||||
pub use volume_oscillator::VolumeOscillator;
|
||||
@@ -1006,6 +1018,12 @@ pub const FAMILIES: &[(&str, &[&str])] = &[
|
||||
"YangZhangVolatility",
|
||||
"JumpIndicator",
|
||||
"RegimeLabel",
|
||||
"EwmaVolatility",
|
||||
"Garch11",
|
||||
"VolatilityOfVolatility",
|
||||
"BipowerVariation",
|
||||
"VolatilityRatio",
|
||||
"VolatilityCone",
|
||||
],
|
||||
),
|
||||
(
|
||||
@@ -1423,6 +1441,6 @@ mod family_tests {
|
||||
// the actual indicator count is the early-warning signal that an
|
||||
// indicator was added without being assigned a family.
|
||||
let total: usize = FAMILIES.iter().map(|(_, ns)| ns.len()).sum();
|
||||
assert_eq!(total, 423, "FAMILIES total drifted from indicator count");
|
||||
assert_eq!(total, 429, "FAMILIES total drifted from indicator count");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,381 @@
|
||||
//! Volatility Cone — current realized volatility within its historical envelope.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Output of [`VolatilityCone`]: the current realized volatility together with
|
||||
/// the envelope (the "cone") it sits inside over the lookback window.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct VolatilityConeOutput {
|
||||
/// Latest realized volatility (sample stddev of log returns over `window`).
|
||||
pub current: f64,
|
||||
/// Lowest realized volatility seen over the `lookback` window.
|
||||
pub min: f64,
|
||||
/// Median realized volatility over the `lookback` window.
|
||||
pub median: f64,
|
||||
/// Highest realized volatility seen over the `lookback` window.
|
||||
pub max: f64,
|
||||
/// Percentile rank of `current` within the lookback distribution, in
|
||||
/// `[0, 100]` — the share of stored volatilities `<= current`, times 100.
|
||||
pub percentile: f64,
|
||||
}
|
||||
|
||||
/// Sample standard deviation from a running `(sum, sum_of_squares, count)`.
|
||||
fn sample_stddev(sum: f64, sum_sq: f64, count: usize) -> f64 {
|
||||
let n = count as f64;
|
||||
let mean = sum / n;
|
||||
let variance = ((sum_sq - n * mean * mean) / (n - 1.0)).max(0.0);
|
||||
variance.sqrt()
|
||||
}
|
||||
|
||||
/// Volatility Cone — the current realized volatility positioned within the
|
||||
/// historical range ("cone") of realized volatilities over a lookback window.
|
||||
///
|
||||
/// ```text
|
||||
/// r_t = ln(close_t / close_{t−1})
|
||||
/// vol_t = stddev_sample(r over window) (rolling realized volatility)
|
||||
/// cone = { min, median, max, percentile } of vol over the last `lookback`
|
||||
/// ```
|
||||
///
|
||||
/// A volatility cone (Burghardt & Lane 1990) shows whether current volatility is
|
||||
/// high or low *relative to its own history*, rather than as an absolute number.
|
||||
/// This streaming form tracks one horizon: it maintains the rolling realized
|
||||
/// volatility of log returns over `window`, then reports the latest reading
|
||||
/// (`current`) alongside the `min`, `median`, `max` and percentile rank of that
|
||||
/// volatility series over the trailing `lookback`. `current` always lies within
|
||||
/// `[min, max]` because it is itself the newest member of the lookback set.
|
||||
///
|
||||
/// Only the candle's **close** is used (the log-return series); the high and low
|
||||
/// are ignored. The volatility is per-period (sample stddev of log returns, not
|
||||
/// annualised) — multiply by `√trading_periods` for an annual figure. Each
|
||||
/// `update` is O(`lookback log lookback`) from sorting the envelope.
|
||||
///
|
||||
/// Non-positive closes are ignored (the log return would be undefined): the tick
|
||||
/// is dropped, state is left untouched, and the last value is returned.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Candle, Indicator, VolatilityCone};
|
||||
///
|
||||
/// let mut indicator = VolatilityCone::new(20, 60).unwrap();
|
||||
/// let mut last = None;
|
||||
/// for i in 0..120 {
|
||||
/// let c = 100.0 + (f64::from(i) * 0.3).sin() * 5.0;
|
||||
/// let candle = Candle::new(c, c + 1.0, c - 1.0, c, 1_000.0, 0).unwrap();
|
||||
/// last = indicator.update(candle);
|
||||
/// }
|
||||
/// assert!(last.is_some());
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct VolatilityCone {
|
||||
window: usize,
|
||||
lookback: usize,
|
||||
prev_close: Option<f64>,
|
||||
/// Rolling window of log returns for the inner realized-volatility series.
|
||||
returns: VecDeque<f64>,
|
||||
ret_sum: f64,
|
||||
ret_sum_sq: f64,
|
||||
/// Rolling window of realized-volatility readings (the cone envelope).
|
||||
vols: VecDeque<f64>,
|
||||
last: Option<VolatilityConeOutput>,
|
||||
}
|
||||
|
||||
impl VolatilityCone {
|
||||
/// Construct a new volatility-cone indicator.
|
||||
///
|
||||
/// `window` is the realized-volatility estimation window; `lookback` is the
|
||||
/// number of volatility readings forming the historical cone.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns [`Error::PeriodZero`] if either argument is `0`, or
|
||||
/// [`Error::InvalidPeriod`] if `window < 2` (a sample stddev needs two
|
||||
/// returns) or `lookback < 2` (an envelope needs at least two readings).
|
||||
pub fn new(window: usize, lookback: usize) -> Result<Self> {
|
||||
if window == 0 || lookback == 0 {
|
||||
return Err(Error::PeriodZero);
|
||||
}
|
||||
if window < 2 || lookback < 2 {
|
||||
return Err(Error::InvalidPeriod {
|
||||
message: "volatility cone window and lookback must both be >= 2",
|
||||
});
|
||||
}
|
||||
Ok(Self {
|
||||
window,
|
||||
lookback,
|
||||
prev_close: None,
|
||||
returns: VecDeque::with_capacity(window),
|
||||
ret_sum: 0.0,
|
||||
ret_sum_sq: 0.0,
|
||||
vols: VecDeque::with_capacity(lookback),
|
||||
last: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured `(window, lookback)`.
|
||||
pub const fn windows(&self) -> (usize, usize) {
|
||||
(self.window, self.lookback)
|
||||
}
|
||||
|
||||
/// Current value if available.
|
||||
pub const fn value(&self) -> Option<VolatilityConeOutput> {
|
||||
self.last
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for VolatilityCone {
|
||||
type Input = Candle;
|
||||
type Output = VolatilityConeOutput;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<VolatilityConeOutput> {
|
||||
let price = candle.close;
|
||||
// A log return is undefined for a non-positive close; skip the tick.
|
||||
if price <= 0.0 {
|
||||
return self.last;
|
||||
}
|
||||
let Some(prev) = self.prev_close else {
|
||||
self.prev_close = Some(price);
|
||||
return None;
|
||||
};
|
||||
self.prev_close = Some(price);
|
||||
// `prev` came from `self.prev_close`, gated by the guard above, so it is
|
||||
// positive — the log return is always well-defined.
|
||||
let r = (price / prev).ln();
|
||||
|
||||
// Stage one: rolling sample volatility of log returns.
|
||||
if self.returns.len() == self.window {
|
||||
let old = self.returns.pop_front().expect("returns window non-empty");
|
||||
self.ret_sum -= old;
|
||||
self.ret_sum_sq -= old * old;
|
||||
}
|
||||
self.returns.push_back(r);
|
||||
self.ret_sum += r;
|
||||
self.ret_sum_sq += r * r;
|
||||
if self.returns.len() < self.window {
|
||||
return None;
|
||||
}
|
||||
let current = sample_stddev(self.ret_sum, self.ret_sum_sq, self.window);
|
||||
|
||||
// Stage two: maintain the lookback envelope of volatility readings.
|
||||
if self.vols.len() == self.lookback {
|
||||
self.vols.pop_front();
|
||||
}
|
||||
self.vols.push_back(current);
|
||||
if self.vols.len() < self.lookback {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut sorted: Vec<f64> = self.vols.iter().copied().collect();
|
||||
sorted.sort_by(f64::total_cmp);
|
||||
let min = sorted[0];
|
||||
let max = sorted[self.lookback - 1];
|
||||
let mid = self.lookback / 2;
|
||||
let median = if self.lookback % 2 == 1 {
|
||||
sorted[mid]
|
||||
} else {
|
||||
f64::midpoint(sorted[mid - 1], sorted[mid])
|
||||
};
|
||||
let count_le = self.vols.iter().filter(|&&v| v <= current).count();
|
||||
let percentile = count_le as f64 / self.lookback as f64 * 100.0;
|
||||
|
||||
let out = VolatilityConeOutput {
|
||||
current,
|
||||
min,
|
||||
median,
|
||||
max,
|
||||
percentile,
|
||||
};
|
||||
self.last = Some(out);
|
||||
Some(out)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.prev_close = None;
|
||||
self.returns.clear();
|
||||
self.ret_sum = 0.0;
|
||||
self.ret_sum_sq = 0.0;
|
||||
self.vols.clear();
|
||||
self.last = None;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
// One previous close for the first return, `window` returns for the
|
||||
// first volatility, then `lookback` volatilities for the envelope.
|
||||
self.window + self.lookback
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.last.is_some()
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"VolatilityCone"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
/// Candle whose close drives the indicator (open = high = low = close here).
|
||||
fn close_candle(close: f64) -> Candle {
|
||||
Candle::new_unchecked(close, close, close, close, 1_000.0, 0)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_zero_window() {
|
||||
assert!(matches!(VolatilityCone::new(0, 10), Err(Error::PeriodZero)));
|
||||
assert!(matches!(VolatilityCone::new(10, 0), Err(Error::PeriodZero)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_window_one() {
|
||||
assert!(matches!(
|
||||
VolatilityCone::new(1, 10),
|
||||
Err(Error::InvalidPeriod { .. })
|
||||
));
|
||||
assert!(matches!(
|
||||
VolatilityCone::new(10, 1),
|
||||
Err(Error::InvalidPeriod { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let vc = VolatilityCone::new(20, 60).unwrap();
|
||||
assert_eq!(vc.windows(), (20, 60));
|
||||
assert_eq!(vc.warmup_period(), 80);
|
||||
assert_eq!(vc.name(), "VolatilityCone");
|
||||
assert!(!vc.is_ready());
|
||||
assert_eq!(vc.value(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_emission_at_warmup_period() {
|
||||
let mut vc = VolatilityCone::new(2, 2).unwrap();
|
||||
let prices = [100.0, 110.0, 121.0, 100.0, 105.0, 99.0];
|
||||
let candles: Vec<Candle> = prices.iter().map(|p| close_candle(*p)).collect();
|
||||
let out = vc.batch(&candles);
|
||||
let warmup = vc.warmup_period(); // 4
|
||||
assert_eq!(warmup, 4);
|
||||
for v in out.iter().take(warmup - 1) {
|
||||
assert!(v.is_none());
|
||||
}
|
||||
assert!(out[warmup - 1].is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn known_value() {
|
||||
// window = 2 -> vol = |r_t − r_{t−1}| / √2; lookback = 2.
|
||||
// prices: r1 = r2 = ln(1.1), r3 = ln(100/121).
|
||||
let mut vc = VolatilityCone::new(2, 2).unwrap();
|
||||
let candles: Vec<Candle> = [100.0, 110.0, 121.0, 100.0]
|
||||
.iter()
|
||||
.map(|p| close_candle(*p))
|
||||
.collect();
|
||||
let out = vc.batch(&candles);
|
||||
let r2 = (121.0_f64 / 110.0).ln();
|
||||
let r3 = (100.0_f64 / 121.0).ln();
|
||||
let vol2 = (r2 - r3).abs() / 2.0_f64.sqrt();
|
||||
let o = out[3].unwrap();
|
||||
assert_relative_eq!(o.current, vol2, epsilon = 1e-9);
|
||||
assert_relative_eq!(o.min, 0.0, epsilon = 1e-9); // vol1 = 0 (r1 == r2)
|
||||
assert_relative_eq!(o.max, vol2, epsilon = 1e-9);
|
||||
assert_relative_eq!(o.median, vol2 / 2.0, epsilon = 1e-9);
|
||||
assert_relative_eq!(o.percentile, 100.0, epsilon = 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn odd_lookback_median_is_middle() {
|
||||
// lookback = 3 picks the middle of the sorted envelope.
|
||||
let mut vc = VolatilityCone::new(2, 3).unwrap();
|
||||
let candles: Vec<Candle> = [100.0, 101.0, 103.0, 100.0, 104.0, 99.0, 106.0]
|
||||
.iter()
|
||||
.map(|p| close_candle(*p))
|
||||
.collect();
|
||||
let out = vc.batch(&candles);
|
||||
let o = out.last().unwrap().unwrap();
|
||||
assert!(o.min <= o.median && o.median <= o.max);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn envelope_brackets_current() {
|
||||
let mut vc = VolatilityCone::new(10, 30).unwrap();
|
||||
let candles: Vec<Candle> = (0..200)
|
||||
.map(|i| close_candle(100.0 + (f64::from(i) * 0.3).sin() * 12.0))
|
||||
.collect();
|
||||
for o in vc.batch(&candles).into_iter().flatten() {
|
||||
assert!(o.min <= o.current && o.current <= o.max);
|
||||
assert!(o.min <= o.median && o.median <= o.max);
|
||||
assert!(o.percentile > 0.0 && o.percentile <= 100.0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn constant_series_yields_zero_cone() {
|
||||
let mut vc = VolatilityCone::new(5, 5).unwrap();
|
||||
let candles: Vec<Candle> = (0..40).map(|_| close_candle(100.0)).collect();
|
||||
for o in vc.batch(&candles).into_iter().flatten() {
|
||||
assert_relative_eq!(o.current, 0.0, epsilon = 1e-12);
|
||||
assert_relative_eq!(o.min, 0.0, epsilon = 1e-12);
|
||||
assert_relative_eq!(o.max, 0.0, epsilon = 1e-12);
|
||||
assert_relative_eq!(o.median, 0.0, epsilon = 1e-12);
|
||||
assert_relative_eq!(o.percentile, 100.0, epsilon = 1e-12);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_non_positive_close() {
|
||||
let mut vc = VolatilityCone::new(2, 2).unwrap();
|
||||
let candles: Vec<Candle> = [100.0, 110.0, 121.0, 100.0]
|
||||
.iter()
|
||||
.map(|p| close_candle(*p))
|
||||
.collect();
|
||||
let warmup = vc.batch(&candles);
|
||||
let baseline = warmup.last().copied().flatten().expect("warmed up");
|
||||
// A non-positive close is skipped and the previous value is returned.
|
||||
assert_eq!(vc.update(close_candle(0.0)), Some(baseline));
|
||||
// State untouched: a clone advanced by the same real tick agrees.
|
||||
let mut control = vc.clone();
|
||||
let after = vc.update(close_candle(105.0)).expect("ready");
|
||||
assert_eq!(control.update(close_candle(105.0)).expect("ready"), after);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_non_positive_before_first_close() {
|
||||
let mut vc = VolatilityCone::new(2, 2).unwrap();
|
||||
assert_eq!(vc.update(close_candle(0.0)), None);
|
||||
assert_eq!(vc.update(close_candle(100.0)), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut vc = VolatilityCone::new(2, 2).unwrap();
|
||||
let candles: Vec<Candle> = [100.0, 110.0, 121.0, 100.0, 105.0]
|
||||
.iter()
|
||||
.map(|p| close_candle(*p))
|
||||
.collect();
|
||||
vc.batch(&candles);
|
||||
assert!(vc.is_ready());
|
||||
vc.reset();
|
||||
assert!(!vc.is_ready());
|
||||
assert_eq!(vc.value(), None);
|
||||
assert_eq!(vc.update(close_candle(100.0)), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles: Vec<Candle> = (0..200)
|
||||
.map(|i| close_candle(100.0 + (f64::from(i) * 0.25).sin() * 9.0))
|
||||
.collect();
|
||||
let batch = VolatilityCone::new(10, 30).unwrap().batch(&candles);
|
||||
let mut b = VolatilityCone::new(10, 30).unwrap();
|
||||
let streamed: Vec<_> = candles.iter().map(|c| b.update(*c)).collect();
|
||||
assert_eq!(batch, streamed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,333 @@
|
||||
//! Volatility of Volatility — the dispersion of a rolling volatility series.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Sample standard deviation from a running `(sum, sum_of_squares, count)`.
|
||||
///
|
||||
/// Uses Bessel's correction (divisor `n − 1`) and clamps a tiny negative
|
||||
/// floating-point residual to zero before the square root.
|
||||
fn sample_stddev(sum: f64, sum_sq: f64, count: usize) -> f64 {
|
||||
let n = count as f64;
|
||||
let mean = sum / n;
|
||||
let variance = ((sum_sq - n * mean * mean) / (n - 1.0)).max(0.0);
|
||||
variance.sqrt()
|
||||
}
|
||||
|
||||
/// Volatility of Volatility — the standard deviation of a rolling realized-
|
||||
/// volatility series ("vol-of-vol").
|
||||
///
|
||||
/// ```text
|
||||
/// r_t = ln(price_t / price_{t−1})
|
||||
/// vol_t = stddev_sample(r over vol_window) (rolling realized volatility)
|
||||
/// VoV = stddev_sample(vol over vov_window) (dispersion of that series)
|
||||
/// ```
|
||||
///
|
||||
/// This is a two-stage estimator: the first stage measures the rolling sample
|
||||
/// volatility of log returns (the same quantity
|
||||
/// [`HistoricalVolatility`](crate::HistoricalVolatility) annualises), and the
|
||||
/// second stage measures how much *that* volatility itself moves. A high
|
||||
/// vol-of-vol means the volatility regime is unstable — turbulent periods
|
||||
/// alternate with calm ones — which is exactly the convexity that long-gamma and
|
||||
/// volatility-trading strategies care about. Both stages use the unbiased
|
||||
/// `n − 1` sample standard deviation. Each `update` is O(1).
|
||||
///
|
||||
/// Non-finite and non-positive prices are ignored (the log return would be
|
||||
/// undefined): the tick is dropped, state is left untouched, and the last value
|
||||
/// is returned.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Indicator, VolatilityOfVolatility};
|
||||
///
|
||||
/// let mut indicator = VolatilityOfVolatility::new(20, 20).unwrap();
|
||||
/// let mut last = None;
|
||||
/// for i in 0..120 {
|
||||
/// last = indicator.update(100.0 + (f64::from(i) * 0.3).sin() * 5.0);
|
||||
/// }
|
||||
/// assert!(last.is_some());
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct VolatilityOfVolatility {
|
||||
vol_window: usize,
|
||||
vov_window: usize,
|
||||
prev_price: Option<f64>,
|
||||
/// Rolling window of log returns (stage one).
|
||||
returns: VecDeque<f64>,
|
||||
ret_sum: f64,
|
||||
ret_sum_sq: f64,
|
||||
/// Rolling window of realized-volatility readings (stage two).
|
||||
vols: VecDeque<f64>,
|
||||
vol_sum: f64,
|
||||
vol_sum_sq: f64,
|
||||
last: Option<f64>,
|
||||
}
|
||||
|
||||
impl VolatilityOfVolatility {
|
||||
/// Construct a new vol-of-vol indicator.
|
||||
///
|
||||
/// `vol_window` is the window for the inner realized-volatility series;
|
||||
/// `vov_window` is the window over which its dispersion is measured.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns [`Error::PeriodZero`] if either window is `0`, or
|
||||
/// [`Error::InvalidPeriod`] if either is `1` (a sample standard deviation
|
||||
/// needs at least two observations).
|
||||
pub fn new(vol_window: usize, vov_window: usize) -> Result<Self> {
|
||||
if vol_window == 0 || vov_window == 0 {
|
||||
return Err(Error::PeriodZero);
|
||||
}
|
||||
if vol_window < 2 || vov_window < 2 {
|
||||
return Err(Error::InvalidPeriod {
|
||||
message: "vol-of-vol windows must both be >= 2",
|
||||
});
|
||||
}
|
||||
Ok(Self {
|
||||
vol_window,
|
||||
vov_window,
|
||||
prev_price: None,
|
||||
returns: VecDeque::with_capacity(vol_window),
|
||||
ret_sum: 0.0,
|
||||
ret_sum_sq: 0.0,
|
||||
vols: VecDeque::with_capacity(vov_window),
|
||||
vol_sum: 0.0,
|
||||
vol_sum_sq: 0.0,
|
||||
last: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured `(vol_window, vov_window)`.
|
||||
pub const fn windows(&self) -> (usize, usize) {
|
||||
(self.vol_window, self.vov_window)
|
||||
}
|
||||
|
||||
/// Current value if available.
|
||||
pub const fn value(&self) -> Option<f64> {
|
||||
self.last
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for VolatilityOfVolatility {
|
||||
type Input = f64;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, input: f64) -> Option<f64> {
|
||||
// Non-finite / non-positive prices are skipped: `ln(input / prev)` is
|
||||
// undefined, so the tick must not enter the return window.
|
||||
if !input.is_finite() || input <= 0.0 {
|
||||
return self.last;
|
||||
}
|
||||
let Some(prev) = self.prev_price else {
|
||||
self.prev_price = Some(input);
|
||||
return None;
|
||||
};
|
||||
self.prev_price = Some(input);
|
||||
// `prev` came from `self.prev_price`, gated by the guard above, so it is
|
||||
// finite and positive — the log return is always well-defined.
|
||||
let r = (input / prev).ln();
|
||||
|
||||
// Stage one: rolling sample volatility of log returns.
|
||||
if self.returns.len() == self.vol_window {
|
||||
let old = self.returns.pop_front().expect("returns window non-empty");
|
||||
self.ret_sum -= old;
|
||||
self.ret_sum_sq -= old * old;
|
||||
}
|
||||
self.returns.push_back(r);
|
||||
self.ret_sum += r;
|
||||
self.ret_sum_sq += r * r;
|
||||
if self.returns.len() < self.vol_window {
|
||||
return None;
|
||||
}
|
||||
let vol = sample_stddev(self.ret_sum, self.ret_sum_sq, self.vol_window);
|
||||
|
||||
// Stage two: rolling sample dispersion of the volatility series.
|
||||
if self.vols.len() == self.vov_window {
|
||||
let old = self.vols.pop_front().expect("vols window non-empty");
|
||||
self.vol_sum -= old;
|
||||
self.vol_sum_sq -= old * old;
|
||||
}
|
||||
self.vols.push_back(vol);
|
||||
self.vol_sum += vol;
|
||||
self.vol_sum_sq += vol * vol;
|
||||
if self.vols.len() < self.vov_window {
|
||||
return None;
|
||||
}
|
||||
let vov = sample_stddev(self.vol_sum, self.vol_sum_sq, self.vov_window);
|
||||
self.last = Some(vov);
|
||||
Some(vov)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.prev_price = None;
|
||||
self.returns.clear();
|
||||
self.ret_sum = 0.0;
|
||||
self.ret_sum_sq = 0.0;
|
||||
self.vols.clear();
|
||||
self.vol_sum = 0.0;
|
||||
self.vol_sum_sq = 0.0;
|
||||
self.last = None;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
// One previous price for the first return, `vol_window` returns for the
|
||||
// first volatility, then `vov_window` volatilities for the dispersion.
|
||||
// The two windows overlap on the bar axis, so this is the sum.
|
||||
self.vol_window + self.vov_window
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.last.is_some()
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"VolatilityOfVolatility"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
use crate::HistoricalVolatility;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
#[test]
|
||||
fn rejects_zero_window() {
|
||||
assert!(matches!(
|
||||
VolatilityOfVolatility::new(0, 10),
|
||||
Err(Error::PeriodZero)
|
||||
));
|
||||
assert!(matches!(
|
||||
VolatilityOfVolatility::new(10, 0),
|
||||
Err(Error::PeriodZero)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_window_one() {
|
||||
assert!(matches!(
|
||||
VolatilityOfVolatility::new(1, 10),
|
||||
Err(Error::InvalidPeriod { .. })
|
||||
));
|
||||
assert!(matches!(
|
||||
VolatilityOfVolatility::new(10, 1),
|
||||
Err(Error::InvalidPeriod { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let vov = VolatilityOfVolatility::new(20, 10).unwrap();
|
||||
assert_eq!(vov.windows(), (20, 10));
|
||||
assert_eq!(vov.warmup_period(), 30);
|
||||
assert_eq!(vov.name(), "VolatilityOfVolatility");
|
||||
assert!(!vov.is_ready());
|
||||
assert_eq!(vov.value(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_emission_at_warmup_period() {
|
||||
let mut vov = VolatilityOfVolatility::new(3, 3).unwrap();
|
||||
let prices: Vec<f64> = (1..=20)
|
||||
.map(|i| 100.0 + (f64::from(i) * 0.7).sin() * 4.0)
|
||||
.collect();
|
||||
let out = vov.batch(&prices);
|
||||
let warmup = vov.warmup_period(); // 6
|
||||
for v in out.iter().take(warmup - 1) {
|
||||
assert!(v.is_none());
|
||||
}
|
||||
assert!(out[warmup - 1].is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn matches_two_stage_reference() {
|
||||
// Stage one equals HistoricalVolatility(vol_window, 1) / 100 (sample
|
||||
// stddev of log returns); stage two is the sample stddev of that series.
|
||||
let (vol_window, vov_window) = (3, 3);
|
||||
let prices: Vec<f64> = [100.0, 102.0, 101.0, 104.0, 103.5, 106.0, 105.0, 108.0].to_vec();
|
||||
|
||||
let mut hv = HistoricalVolatility::new(vol_window, 1).unwrap();
|
||||
let vol_series: Vec<f64> = hv
|
||||
.batch(&prices)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|v| v / 100.0)
|
||||
.collect();
|
||||
// Sample stddev of the last `vov_window` volatilities.
|
||||
let tail = &vol_series[vol_series.len() - vov_window..];
|
||||
let sum: f64 = tail.iter().sum();
|
||||
let sum_sq: f64 = tail.iter().map(|v| v * v).sum();
|
||||
let expected = sample_stddev(sum, sum_sq, vov_window);
|
||||
|
||||
let mut vov = VolatilityOfVolatility::new(vol_window, vov_window).unwrap();
|
||||
let out = vov.batch(&prices);
|
||||
assert_relative_eq!(out.last().unwrap().unwrap(), expected, epsilon = 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn constant_series_yields_zero() {
|
||||
let mut vov = VolatilityOfVolatility::new(5, 5).unwrap();
|
||||
for v in vov.batch(&[100.0; 60]).into_iter().flatten() {
|
||||
assert_relative_eq!(v, 0.0, epsilon = 1e-12);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn output_is_non_negative() {
|
||||
let mut vov = VolatilityOfVolatility::new(10, 10).unwrap();
|
||||
let prices: Vec<f64> = (1..=300)
|
||||
.map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 12.0)
|
||||
.collect();
|
||||
for v in vov.batch(&prices).into_iter().flatten() {
|
||||
assert!(v >= 0.0, "vol-of-vol must be non-negative, got {v}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_non_finite_input() {
|
||||
let mut vov = VolatilityOfVolatility::new(3, 3).unwrap();
|
||||
let out = vov.batch(&(1..=40).map(f64::from).collect::<Vec<_>>());
|
||||
let last = *out.last().unwrap();
|
||||
assert!(last.is_some());
|
||||
assert_eq!(vov.update(f64::NAN), last);
|
||||
assert_eq!(vov.update(f64::INFINITY), last);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_non_positive_prices() {
|
||||
let mut vov = VolatilityOfVolatility::new(3, 3).unwrap();
|
||||
let warmup = vov.batch(&(1..=40).map(f64::from).collect::<Vec<_>>());
|
||||
let baseline = warmup.last().copied().flatten().expect("warmed up");
|
||||
assert_eq!(vov.update(-5.0), Some(baseline));
|
||||
assert_eq!(vov.update(0.0), Some(baseline));
|
||||
// State untouched: a clone advanced by the same real tick agrees.
|
||||
let mut control = vov.clone();
|
||||
let after = vov.update(41.0).expect("ready");
|
||||
assert_eq!(control.update(41.0).expect("ready"), after);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut vov = VolatilityOfVolatility::new(3, 3).unwrap();
|
||||
vov.batch(&(1..=40).map(f64::from).collect::<Vec<_>>());
|
||||
assert!(vov.is_ready());
|
||||
vov.reset();
|
||||
assert!(!vov.is_ready());
|
||||
assert_eq!(vov.value(), None);
|
||||
assert_eq!(vov.update(1.0), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let prices: Vec<f64> = (1..=200)
|
||||
.map(|i| 100.0 + (f64::from(i) * 0.25).sin() * 9.0)
|
||||
.collect();
|
||||
let batch = VolatilityOfVolatility::new(10, 10).unwrap().batch(&prices);
|
||||
let mut b = VolatilityOfVolatility::new(10, 10).unwrap();
|
||||
let streamed: Vec<_> = prices.iter().map(|p| b.update(*p)).collect();
|
||||
assert_eq!(batch, streamed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
//! Schwager's Volatility Ratio — today's true range versus its typical level.
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Schwager's Volatility Ratio — the current bar's true range divided by the
|
||||
/// exponential moving average of the *prior* true ranges.
|
||||
///
|
||||
/// ```text
|
||||
/// TR_t = true range of bar t
|
||||
/// VR_t = TR_t / EMA_n(TR through bar t−1)
|
||||
/// ```
|
||||
///
|
||||
/// Jack Schwager's volatility ratio measures how today's range compares to its
|
||||
/// recent typical level: a reading above `2.0` marks a **wide-ranging day** —
|
||||
/// today's true range is more than twice the smoothed average — which often
|
||||
/// precedes or accompanies a reversal. The denominator is the exponential
|
||||
/// moving average of true range *excluding the current bar*, seeded with the
|
||||
/// simple average of the first `period` true ranges, so a single large bar
|
||||
/// stands out instead of inflating its own benchmark.
|
||||
///
|
||||
/// True range is `max(high − low, |high − prev_close|, |low − prev_close|)`,
|
||||
/// identical to the [`Atr`](crate::Atr) building block, but here it is compared
|
||||
/// to a *standard* EMA (smoothing `2 / (period + 1)`) rather than Wilder
|
||||
/// smoothing, which keeps the ratio distinct from `TR / ATR`. Each `update` is
|
||||
/// O(1).
|
||||
///
|
||||
/// A flat market drives every true range — and the EMA — to `0`; the ratio is
|
||||
/// then `0.0` rather than an undefined `0 / 0`. `Candle::new` rejects non-finite
|
||||
/// fields, so no in-method finiteness guard is needed.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Candle, Indicator, VolatilityRatio};
|
||||
///
|
||||
/// let mut indicator = VolatilityRatio::new(14).unwrap();
|
||||
/// let mut last = None;
|
||||
/// for i in 0..40 {
|
||||
/// let base = 100.0 + f64::from(i);
|
||||
/// let candle = Candle::new(base, base + 2.0, base - 1.0, base + 0.5, 1_000.0, 0).unwrap();
|
||||
/// last = indicator.update(candle);
|
||||
/// }
|
||||
/// assert!(last.is_some());
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct VolatilityRatio {
|
||||
period: usize,
|
||||
alpha: f64,
|
||||
prev_close: Option<f64>,
|
||||
/// Sum and count of the first `period` true ranges, used to seed the EMA.
|
||||
seed_sum: f64,
|
||||
seed_count: usize,
|
||||
/// EMA of true range through the previous bar; `None` until seeded.
|
||||
ema: Option<f64>,
|
||||
last: Option<f64>,
|
||||
}
|
||||
|
||||
impl VolatilityRatio {
|
||||
/// Construct a new volatility-ratio indicator.
|
||||
///
|
||||
/// `period` is the number of true ranges that seed and smooth the
|
||||
/// denominator EMA.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns [`Error::PeriodZero`] if `period == 0`.
|
||||
pub fn new(period: usize) -> Result<Self> {
|
||||
if period == 0 {
|
||||
return Err(Error::PeriodZero);
|
||||
}
|
||||
Ok(Self {
|
||||
period,
|
||||
alpha: 2.0 / (period as f64 + 1.0),
|
||||
prev_close: None,
|
||||
seed_sum: 0.0,
|
||||
seed_count: 0,
|
||||
ema: None,
|
||||
last: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured period.
|
||||
pub const fn period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
|
||||
/// Current value if available.
|
||||
pub const fn value(&self) -> Option<f64> {
|
||||
self.last
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for VolatilityRatio {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
// The first bar has no previous close, so no true range can be formed.
|
||||
let Some(prev_close) = self.prev_close else {
|
||||
self.prev_close = Some(candle.close);
|
||||
return None;
|
||||
};
|
||||
let tr = candle.true_range(Some(prev_close));
|
||||
self.prev_close = Some(candle.close);
|
||||
|
||||
match self.ema {
|
||||
None => {
|
||||
// Seeding the EMA with the simple average of the first `period`
|
||||
// true ranges; emit nothing until it is established.
|
||||
self.seed_sum += tr;
|
||||
self.seed_count += 1;
|
||||
if self.seed_count == self.period {
|
||||
self.ema = Some(self.seed_sum / self.period as f64);
|
||||
}
|
||||
None
|
||||
}
|
||||
Some(prev_ema) => {
|
||||
// Denominator excludes the current bar (it is the EMA through the
|
||||
// previous bar). A flat benchmark yields 0.0, not 0/0.
|
||||
let vr = if prev_ema > 0.0 { tr / prev_ema } else { 0.0 };
|
||||
self.ema = Some(self.alpha * tr + (1.0 - self.alpha) * prev_ema);
|
||||
self.last = Some(vr);
|
||||
Some(vr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.prev_close = None;
|
||||
self.seed_sum = 0.0;
|
||||
self.seed_count = 0;
|
||||
self.ema = None;
|
||||
self.last = None;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
// Bar 1 sets the previous close; bars 2..=period+1 seed the EMA; the
|
||||
// first ratio is emitted on bar period + 2.
|
||||
self.period + 2
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.last.is_some()
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"VolatilityRatio"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
/// Build a candle with the given high/low/close (open = low, fixed volume).
|
||||
fn candle(high: f64, low: f64, close: f64) -> Candle {
|
||||
Candle::new_unchecked(low, high, low, close, 1_000.0, 0)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_zero_period() {
|
||||
assert!(matches!(VolatilityRatio::new(0), Err(Error::PeriodZero)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let vr = VolatilityRatio::new(14).unwrap();
|
||||
assert_eq!(vr.period(), 14);
|
||||
assert_eq!(vr.warmup_period(), 16);
|
||||
assert_eq!(vr.name(), "VolatilityRatio");
|
||||
assert!(!vr.is_ready());
|
||||
assert_eq!(vr.value(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_emission_at_warmup_period() {
|
||||
let mut vr = VolatilityRatio::new(3).unwrap();
|
||||
// Build enough constant-range candles to reach warmup.
|
||||
let candles: Vec<Candle> = (0..10)
|
||||
.map(|i| {
|
||||
let base = 100.0 + f64::from(i);
|
||||
candle(base + 1.0, base - 1.0, base)
|
||||
})
|
||||
.collect();
|
||||
let out = vr.batch(&candles);
|
||||
// warmup_period == period + 2 == 5: the first emission is at index 4.
|
||||
let warmup = vr.warmup_period();
|
||||
assert_eq!(warmup, 5);
|
||||
for v in out.iter().take(warmup - 1) {
|
||||
assert!(v.is_none());
|
||||
}
|
||||
assert!(out[warmup - 1].is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wide_ranging_day_exceeds_two() {
|
||||
// Steady true range of 2.0 seeds the EMA, then one bar with a far wider
|
||||
// range pushes the ratio above 2.0.
|
||||
let mut vr = VolatilityRatio::new(3).unwrap();
|
||||
let mut candles: Vec<Candle> = (0..6)
|
||||
.map(|i| {
|
||||
let base = 100.0 + f64::from(i);
|
||||
candle(base + 1.0, base - 1.0, base) // TR = 2.0 each
|
||||
})
|
||||
.collect();
|
||||
// A wide bar: range 10 around the last close (~105).
|
||||
candles.push(candle(110.0, 100.0, 105.0));
|
||||
let out = vr.batch(&candles);
|
||||
let last = out.last().unwrap().unwrap();
|
||||
assert!(last > 2.0, "wide-ranging day should exceed 2.0, got {last}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn steady_range_ratio_is_one() {
|
||||
// Constant true range -> EMA equals it -> ratio is exactly 1.0.
|
||||
let mut vr = VolatilityRatio::new(3).unwrap();
|
||||
let candles: Vec<Candle> = (0..12)
|
||||
.map(|i| {
|
||||
let base = 100.0 + f64::from(i);
|
||||
candle(base + 1.0, base - 1.0, base) // TR = 2.0 each
|
||||
})
|
||||
.collect();
|
||||
let out = vr.batch(&candles);
|
||||
assert_relative_eq!(out.last().unwrap().unwrap(), 1.0, epsilon = 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flat_market_yields_zero() {
|
||||
// Zero-range candles: TR = 0, EMA = 0, ratio guarded to 0.0.
|
||||
let mut vr = VolatilityRatio::new(3).unwrap();
|
||||
let candles: Vec<Candle> = (0..10).map(|_| candle(100.0, 100.0, 100.0)).collect();
|
||||
let out = vr.batch(&candles);
|
||||
for v in out.into_iter().flatten() {
|
||||
assert_relative_eq!(v, 0.0, epsilon = 1e-12);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn output_is_non_negative() {
|
||||
let mut vr = VolatilityRatio::new(14).unwrap();
|
||||
let candles: Vec<Candle> = (0..200)
|
||||
.map(|i| {
|
||||
let base = 100.0 + (f64::from(i) * 0.3).sin() * 12.0;
|
||||
candle(base + 2.0, base - 2.0, base + 0.5)
|
||||
})
|
||||
.collect();
|
||||
for v in vr.batch(&candles).into_iter().flatten() {
|
||||
assert!(v >= 0.0, "volatility ratio must be non-negative, got {v}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut vr = VolatilityRatio::new(3).unwrap();
|
||||
let candles: Vec<Candle> = (0..10)
|
||||
.map(|i| {
|
||||
let base = 100.0 + f64::from(i);
|
||||
candle(base + 1.0, base - 1.0, base)
|
||||
})
|
||||
.collect();
|
||||
vr.batch(&candles);
|
||||
assert!(vr.is_ready());
|
||||
vr.reset();
|
||||
assert!(!vr.is_ready());
|
||||
assert_eq!(vr.value(), None);
|
||||
assert_eq!(vr.update(candle(101.0, 99.0, 100.0)), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles: Vec<Candle> = (0..120)
|
||||
.map(|i| {
|
||||
let base = 100.0 + (f64::from(i) * 0.25).sin() * 9.0;
|
||||
candle(base + 2.0, base - 1.5, base + 0.5)
|
||||
})
|
||||
.collect();
|
||||
let batch = VolatilityRatio::new(14).unwrap().batch(&candles);
|
||||
let mut b = VolatilityRatio::new(14).unwrap();
|
||||
let streamed: Vec<_> = candles.iter().map(|c| b.update(*c)).collect();
|
||||
assert_eq!(batch, streamed);
|
||||
}
|
||||
}
|
||||
@@ -63,9 +63,9 @@ pub use indicators::{
|
||||
AroonOscillator, AroonOutput, Atr, AtrBands, AtrBandsOutput, AtrTrailingStop, AutoFib,
|
||||
AutoFibOutput, Autocorrelation, AverageDailyRange, AverageDrawdown, AvgPrice,
|
||||
AwesomeOscillator, AwesomeOscillatorHistogram, BalanceOfPower, Bat, BeltHold, Beta,
|
||||
BetaNeutralSpread, BodySizePct, BollingerBands, BollingerBandwidth, BollingerOutput,
|
||||
BreadthThrust, Breakaway, BullishPercentIndex, Butterfly, CalendarSpread, CalmarRatio,
|
||||
Camarilla, CamarillaPivotsOutput, Cci, CenterOfGravity, Cfo, ChaikinMoneyFlow,
|
||||
BetaNeutralSpread, BipowerVariation, BodySizePct, BollingerBands, BollingerBandwidth,
|
||||
BollingerOutput, BreadthThrust, Breakaway, BullishPercentIndex, Butterfly, CalendarSpread,
|
||||
CalmarRatio, Camarilla, CamarillaPivotsOutput, Cci, CenterOfGravity, Cfo, ChaikinMoneyFlow,
|
||||
ChaikinOscillator, ChaikinVolatility, ChandeKrollStop, ChandeKrollStopOutput, ChandelierExit,
|
||||
ChandelierExitOutput, ChoppinessIndex, ClassicPivots, ClassicPivotsOutput, CloseVsOpen,
|
||||
ClosingMarubozu, Cmo, CoefficientOfVariation, Cointegration, CointegrationOutput,
|
||||
@@ -77,66 +77,68 @@ pub use indicators::{
|
||||
DonchianStopOutput, DoubleBollinger, DoubleBollingerOutput, DoubleTopBottom,
|
||||
DownsideGapThreeMethods, Dpo, DragonflyDoji, DrawdownDuration, Dx, DynamicMomentumIndex,
|
||||
EaseOfMovement, EffectiveSpread, EhlersStochastic, Ehma, ElderImpulse, ElderRay,
|
||||
ElderRayOutput, Ema, EmpiricalModeDecomposition, Engulfing, EveningDojiStar, Evwma, Expectancy,
|
||||
FallingThreeMethods, Fama, FibArcs, FibArcsOutput, FibChannel, FibChannelOutput, FibConfluence,
|
||||
FibConfluenceOutput, FibExtension, FibExtensionOutput, FibFan, FibFanOutput, FibProjection,
|
||||
FibProjectionOutput, FibRetracement, FibRetracementOutput, FibTimeZones, FibTimeZonesOutput,
|
||||
FibonacciPivots, FibonacciPivotsOutput, FisherRsi, FisherTransform, FlagPennant, Footprint,
|
||||
FootprintOutput, ForceIndex, FractalChaosBands, FractalChaosBandsOutput, Frama, FundingBasis,
|
||||
FundingRate, FundingRateMean, FundingRateZScore, GainLossRatio, GapSideBySideWhite,
|
||||
GarmanKlassVolatility, Gartley, GatorOscillator, GatorOscillatorOutput, GeneralizedDema,
|
||||
GeometricMa, GoldenPocket, GoldenPocketOutput, GrangerCausality, GravestoneDoji, Hammer,
|
||||
HangingMan, Harami, HeadAndShoulders, HeikinAshi, HeikinAshiOutput, HiLoActivator,
|
||||
HighLowIndex, HighLowRange, HighWave, Hikkake, HikkakeModified, HilbertDominantCycle,
|
||||
HistoricalVolatility, Hma, HoltWinters, HomingPigeon, HtDcPhase, HtPhasor, HtPhasorOutput,
|
||||
HtTrendMode, HurstChannel, HurstChannelOutput, HurstExponent, Ichimoku, IchimokuOutput,
|
||||
IdenticalThreeCrows, InNeck, Inertia, InformationRatio, InitialBalance, InitialBalanceOutput,
|
||||
InstantaneousTrendline, IntradayMomentumIndex, IntradayVolatilityProfile,
|
||||
IntradayVolatilityProfileOutput, InverseFisherTransform, InvertedHammer, Jma, JumpIndicator,
|
||||
KagiBars, KalmanHedgeRatio, KalmanHedgeRatioOutput, Kama, KasePermissionStochastic,
|
||||
KasePermissionStochasticOutput, KellyCriterion, Keltner, KeltnerOutput, Kicking,
|
||||
KickingByLength, Kst, KstOutput, Kurtosis, Kvo, KylesLambda, LadderBottom, LaguerreRsi,
|
||||
LeadLagCrossCorrelation, LeadLagCrossCorrelationOutput, LinRegAngle, LinRegChannel,
|
||||
LinRegChannelOutput, LinRegIntercept, LinRegSlope, LinearRegression, LiquidationFeatures,
|
||||
LiquidationFeaturesOutput, LogReturn, LongLeggedDoji, LongLine, LongShortRatio, MaEnvelope,
|
||||
MaEnvelopeOutput, MacdExt, MacdFix, MacdHistogram, MacdIndicator, MacdOutput, Mama, MamaOutput,
|
||||
MarketFacilitationIndex, Marubozu, MassIndex, MatHold, MatchingLow, MaxDrawdown,
|
||||
McClellanOscillator, McClellanSummationIndex, McGinleyDynamic, MedianAbsoluteDeviation,
|
||||
MedianMa, MedianPrice, Mfi, Microprice, MidPoint, MidPrice, MinusDi, MinusDm, Mom,
|
||||
MorningDojiStar, MorningEveningStar, Natr, NewHighsNewLows, Nvi, OIPriceDivergence, OIWeighted,
|
||||
Obv, OmegaRatio, OnNeck, OpenInterestDelta, OpeningMarubozu, OpeningRange, OpeningRangeOutput,
|
||||
OrderBookImbalanceFull, OrderBookImbalanceTop1, OrderBookImbalanceTopN, OrderFlowImbalance,
|
||||
OuHalfLife, OvernightGap, OvernightIntradayReturn, OvernightIntradayReturnOutput, PainIndex,
|
||||
PairSpreadZScore, PairwiseBeta, ParkinsonVolatility, PearsonCorrelation, PercentAboveMa,
|
||||
PercentB, PercentageTrailingStop, Pgo, PiercingDarkCloud, PlusDi, PlusDm, Pmo,
|
||||
PointAndFigureBars, PolarizedFractalEfficiency, Ppo, PpoHistogram, ProfitFactor, Psar, Pvi,
|
||||
Qqe, QqeOutput, Qstick, QuotedSpread, RSquared, RealizedSpread, RealizedVolatility,
|
||||
RecoveryFactor, RectangleRange, RegimeLabel, RelativeStrengthAB, RelativeStrengthOutput,
|
||||
RenkoBars, RenkoTrailingStop, RickshawMan, RisingThreeMethods, Rmi, Roc, Rocp, Rocr, Rocr100,
|
||||
RogersSatchellVolatility, RollMeasure, RollingCorrelation, RollingCovariance, RollingIqr,
|
||||
RollingPercentileRank, RollingQuantile, RollingVwap, RoofingFilter, Rsi, Rsx, Rvi,
|
||||
RviVolatility, Rwi, RwiOutput, SarExt, SeasonalZScore, SeparatingLines, SessionHighLow,
|
||||
SessionHighLowOutput, SessionRange, SessionRangeOutput, SessionVwap, Shark, SharpeRatio,
|
||||
ShootingStar, ShortLine, SignedVolume, SineWave, SineWeightedMa, Skewness, Sma, Smi, Smma,
|
||||
SortinoRatio, SpearmanCorrelation, SpinningTop, SpreadAr1Coefficient, SpreadBollingerBands,
|
||||
SpreadBollingerBandsOutput, SpreadHurst, StalledPattern, StandardError, StandardErrorBands,
|
||||
StandardErrorBandsOutput, StarcBands, StarcBandsOutput, Stc, StdDev, StepTrailingStop,
|
||||
StickSandwich, StochRsi, Stochastic, StochasticCci, StochasticOutput, SuperSmoother,
|
||||
SuperTrend, SuperTrendOutput, TakerBuySellRatio, Takuri, TasukiGap, TdCombo, TdCountdown,
|
||||
TdDeMarker, TdDifferential, TdLines, TdLinesOutput, TdOpen, TdPressure, TdRangeProjection,
|
||||
TdRangeProjectionOutput, TdRei, TdRiskLevel, TdRiskLevelOutput, TdSequential,
|
||||
TdSequentialOutput, TdSetup, Tema, TermStructureBasis, ThreeDrives, ThreeInside,
|
||||
ThreeLineStrike, ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth, Thrusting, TickIndex,
|
||||
Tii, TimeOfDayReturnProfile, TimeOfDayReturnProfileOutput, TpoProfile, TpoProfileOutput,
|
||||
TradeImbalance, TrendLabel, TrendStrengthIndex, TreynorRatio, Triangle, Trima, Trin,
|
||||
TripleTopBottom, Trix, TrueRange, Tsf, TsfOscillator, Tsi, Tsv, TtmSqueeze, TtmSqueezeOutput,
|
||||
TtmTrend, TurnOfMonth, Tweezer, TwoCrows, TypicalPrice, UlcerIndex, UltimateOscillator,
|
||||
UniqueThreeRiver, UpDownVolumeRatio, UpsideGapThreeMethods, UpsideGapTwoCrows, ValueArea,
|
||||
ValueAreaOutput, ValueAtRisk, Variance, VarianceRatio, VerticalHorizontalFilter, Vidya,
|
||||
VoltyStop, VolumeByTimeProfile, VolumeByTimeProfileOutput, VolumeOscillator, VolumePriceTrend,
|
||||
VolumeProfile, VolumeProfileOutput, Vortex, VortexOutput, Vpin, Vwap, VwapStdDevBands,
|
||||
VwapStdDevBandsOutput, Vwma, Vzo, WavePm, WaveTrend, WaveTrendOutput, Wedge, WeightedClose,
|
||||
WickRatio, WilliamsFractals, WilliamsFractalsOutput, WilliamsR, WinRate, Wma, WoodiePivots,
|
||||
ElderRayOutput, Ema, EmpiricalModeDecomposition, Engulfing, EveningDojiStar, Evwma,
|
||||
EwmaVolatility, Expectancy, FallingThreeMethods, Fama, FibArcs, FibArcsOutput, FibChannel,
|
||||
FibChannelOutput, FibConfluence, FibConfluenceOutput, FibExtension, FibExtensionOutput, FibFan,
|
||||
FibFanOutput, FibProjection, FibProjectionOutput, FibRetracement, FibRetracementOutput,
|
||||
FibTimeZones, FibTimeZonesOutput, FibonacciPivots, FibonacciPivotsOutput, FisherRsi,
|
||||
FisherTransform, FlagPennant, Footprint, FootprintOutput, ForceIndex, FractalChaosBands,
|
||||
FractalChaosBandsOutput, Frama, FundingBasis, FundingRate, FundingRateMean, FundingRateZScore,
|
||||
GainLossRatio, GapSideBySideWhite, Garch11, GarmanKlassVolatility, Gartley, GatorOscillator,
|
||||
GatorOscillatorOutput, GeneralizedDema, GeometricMa, GoldenPocket, GoldenPocketOutput,
|
||||
GrangerCausality, GravestoneDoji, Hammer, HangingMan, Harami, HeadAndShoulders, HeikinAshi,
|
||||
HeikinAshiOutput, HiLoActivator, HighLowIndex, HighLowRange, HighWave, Hikkake,
|
||||
HikkakeModified, HilbertDominantCycle, HistoricalVolatility, Hma, HoltWinters, HomingPigeon,
|
||||
HtDcPhase, HtPhasor, HtPhasorOutput, HtTrendMode, HurstChannel, HurstChannelOutput,
|
||||
HurstExponent, Ichimoku, IchimokuOutput, IdenticalThreeCrows, InNeck, Inertia,
|
||||
InformationRatio, InitialBalance, InitialBalanceOutput, InstantaneousTrendline,
|
||||
IntradayMomentumIndex, IntradayVolatilityProfile, IntradayVolatilityProfileOutput,
|
||||
InverseFisherTransform, InvertedHammer, Jma, JumpIndicator, KagiBars, KalmanHedgeRatio,
|
||||
KalmanHedgeRatioOutput, Kama, KasePermissionStochastic, KasePermissionStochasticOutput,
|
||||
KellyCriterion, Keltner, KeltnerOutput, Kicking, KickingByLength, Kst, KstOutput, Kurtosis,
|
||||
Kvo, KylesLambda, LadderBottom, LaguerreRsi, LeadLagCrossCorrelation,
|
||||
LeadLagCrossCorrelationOutput, LinRegAngle, LinRegChannel, LinRegChannelOutput,
|
||||
LinRegIntercept, LinRegSlope, LinearRegression, LiquidationFeatures, LiquidationFeaturesOutput,
|
||||
LogReturn, LongLeggedDoji, LongLine, LongShortRatio, MaEnvelope, MaEnvelopeOutput, MacdExt,
|
||||
MacdFix, MacdHistogram, MacdIndicator, MacdOutput, Mama, MamaOutput, MarketFacilitationIndex,
|
||||
Marubozu, MassIndex, MatHold, MatchingLow, MaxDrawdown, McClellanOscillator,
|
||||
McClellanSummationIndex, McGinleyDynamic, MedianAbsoluteDeviation, MedianMa, MedianPrice, Mfi,
|
||||
Microprice, MidPoint, MidPrice, MinusDi, MinusDm, Mom, MorningDojiStar, MorningEveningStar,
|
||||
Natr, NewHighsNewLows, Nvi, OIPriceDivergence, OIWeighted, Obv, OmegaRatio, OnNeck,
|
||||
OpenInterestDelta, OpeningMarubozu, OpeningRange, OpeningRangeOutput, OrderBookImbalanceFull,
|
||||
OrderBookImbalanceTop1, OrderBookImbalanceTopN, OrderFlowImbalance, OuHalfLife, OvernightGap,
|
||||
OvernightIntradayReturn, OvernightIntradayReturnOutput, PainIndex, PairSpreadZScore,
|
||||
PairwiseBeta, ParkinsonVolatility, PearsonCorrelation, PercentAboveMa, PercentB,
|
||||
PercentageTrailingStop, Pgo, PiercingDarkCloud, PlusDi, PlusDm, Pmo, PointAndFigureBars,
|
||||
PolarizedFractalEfficiency, Ppo, PpoHistogram, ProfitFactor, Psar, Pvi, Qqe, QqeOutput, Qstick,
|
||||
QuotedSpread, RSquared, RealizedSpread, RealizedVolatility, RecoveryFactor, RectangleRange,
|
||||
RegimeLabel, RelativeStrengthAB, RelativeStrengthOutput, RenkoBars, RenkoTrailingStop,
|
||||
RickshawMan, RisingThreeMethods, Rmi, Roc, Rocp, Rocr, Rocr100, RogersSatchellVolatility,
|
||||
RollMeasure, RollingCorrelation, RollingCovariance, RollingIqr, RollingPercentileRank,
|
||||
RollingQuantile, RollingVwap, RoofingFilter, Rsi, Rsx, Rvi, RviVolatility, Rwi, RwiOutput,
|
||||
SarExt, SeasonalZScore, SeparatingLines, SessionHighLow, SessionHighLowOutput, SessionRange,
|
||||
SessionRangeOutput, SessionVwap, Shark, SharpeRatio, ShootingStar, ShortLine, SignedVolume,
|
||||
SineWave, SineWeightedMa, Skewness, Sma, Smi, Smma, SortinoRatio, SpearmanCorrelation,
|
||||
SpinningTop, SpreadAr1Coefficient, SpreadBollingerBands, SpreadBollingerBandsOutput,
|
||||
SpreadHurst, StalledPattern, StandardError, StandardErrorBands, StandardErrorBandsOutput,
|
||||
StarcBands, StarcBandsOutput, Stc, StdDev, StepTrailingStop, StickSandwich, StochRsi,
|
||||
Stochastic, StochasticCci, StochasticOutput, SuperSmoother, SuperTrend, SuperTrendOutput,
|
||||
TakerBuySellRatio, Takuri, TasukiGap, TdCombo, TdCountdown, TdDeMarker, TdDifferential,
|
||||
TdLines, TdLinesOutput, TdOpen, TdPressure, TdRangeProjection, TdRangeProjectionOutput, TdRei,
|
||||
TdRiskLevel, TdRiskLevelOutput, TdSequential, TdSequentialOutput, TdSetup, Tema,
|
||||
TermStructureBasis, ThreeDrives, ThreeInside, ThreeLineStrike, ThreeOutside,
|
||||
ThreeSoldiersOrCrows, ThreeStarsInSouth, Thrusting, TickIndex, Tii, TimeOfDayReturnProfile,
|
||||
TimeOfDayReturnProfileOutput, TpoProfile, TpoProfileOutput, TradeImbalance, TrendLabel,
|
||||
TrendStrengthIndex, TreynorRatio, Triangle, Trima, Trin, TripleTopBottom, Trix, TrueRange, Tsf,
|
||||
TsfOscillator, Tsi, Tsv, TtmSqueeze, TtmSqueezeOutput, TtmTrend, TurnOfMonth, Tweezer,
|
||||
TwoCrows, TypicalPrice, UlcerIndex, UltimateOscillator, UniqueThreeRiver, UpDownVolumeRatio,
|
||||
UpsideGapThreeMethods, UpsideGapTwoCrows, ValueArea, ValueAreaOutput, ValueAtRisk, Variance,
|
||||
VarianceRatio, VerticalHorizontalFilter, Vidya, VolatilityCone, VolatilityConeOutput,
|
||||
VolatilityOfVolatility, VolatilityRatio, VoltyStop, VolumeByTimeProfile,
|
||||
VolumeByTimeProfileOutput, VolumeOscillator, VolumePriceTrend, VolumeProfile,
|
||||
VolumeProfileOutput, Vortex, VortexOutput, Vpin, Vwap, VwapStdDevBands, VwapStdDevBandsOutput,
|
||||
Vwma, Vzo, WavePm, WaveTrend, WaveTrendOutput, Wedge, WeightedClose, WickRatio,
|
||||
WilliamsFractals, WilliamsFractalsOutput, WilliamsR, WinRate, Wma, WoodiePivots,
|
||||
WoodiePivotsOutput, YangZhangVolatility, YoyoExit, ZScore, ZeroLagMacd, ZeroLagMacdOutput,
|
||||
ZigZag, ZigZagOutput, Zlema, FAMILIES, T3,
|
||||
};
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ That includes:
|
||||
[Python](https://docs.wickra.org/Quickstart-Python),
|
||||
[Node](https://docs.wickra.org/Quickstart-Node), and
|
||||
[WASM](https://docs.wickra.org/Quickstart-WASM).
|
||||
- A per-indicator deep dive for every one of the **423 indicators** across
|
||||
- A per-indicator deep dive for every one of the **429 indicators** across
|
||||
the sixteen families (Moving Averages, Momentum Oscillators, Trend &
|
||||
Directional, Price Oscillators, Volatility & Bands, Bands & Channels,
|
||||
Trailing Stops, Volume, Price Statistics, Ehlers / Cycle DSP, Pivots &
|
||||
|
||||
Generated
+7
-7
@@ -17,7 +17,7 @@
|
||||
},
|
||||
"../../bindings/node": {
|
||||
"name": "wickra",
|
||||
"version": "0.5.9",
|
||||
"version": "0.6.0",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"devDependencies": {
|
||||
"@napi-rs/cli": "^2.18.0"
|
||||
@@ -26,12 +26,12 @@
|
||||
"node": ">= 18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"wickra-darwin-arm64": "0.5.9",
|
||||
"wickra-darwin-x64": "0.5.9",
|
||||
"wickra-linux-arm64-gnu": "0.5.9",
|
||||
"wickra-linux-x64-gnu": "0.5.9",
|
||||
"wickra-win32-arm64-msvc": "0.5.9",
|
||||
"wickra-win32-x64-msvc": "0.5.9"
|
||||
"wickra-darwin-arm64": "0.6.0",
|
||||
"wickra-darwin-x64": "0.6.0",
|
||||
"wickra-linux-arm64-gnu": "0.6.0",
|
||||
"wickra-linux-x64-gnu": "0.6.0",
|
||||
"wickra-win32-arm64-msvc": "0.6.0",
|
||||
"wickra-win32-x64-msvc": "0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/wickra": {
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
//! `Ema(20)`. This target now covers every scalar indicator in the catalogue.
|
||||
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
use wickra_core::{AdaptiveCycle, AdaptiveLaguerreFilter, Alma, AnchoredRsi, Apo, Autocorrelation, AverageDrawdown, BatchExt, Beta, BollingerBands, CalmarRatio, CenterOfGravity, Cfo, Cmo, CoefficientOfVariation, ConditionalValueAtRisk, ConnorsRsi, Coppock, CyberneticCycle, Decycler, DecyclerOscillator, Dema, DerivativeOscillator, DetrendedStdDev, DisparityIndex, DoubleBollinger, Dpo, DrawdownDuration, DynamicMomentumIndex, EhlersStochastic, Ehma, ElderImpulse, Ema, EmpiricalModeDecomposition, Expectancy, Fama, FisherRsi, FisherTransform, Frama, GainLossRatio, GeneralizedDema, GeometricMa, HilbertDominantCycle, HistoricalVolatility, Hma, HoltWinters, HtDcPhase, HtPhasor, HtTrendMode, HurstExponent, Indicator, InstantaneousTrendline, InverseFisherTransform, Jma, JumpIndicator, Kama, KellyCriterion, Kst, Kurtosis, LaguerreRsi, LinRegAngle, LinRegChannel, LinRegIntercept, LinRegSlope, LinearRegression, LogReturn, MaEnvelope, MaType, MacdExt, MacdFix, MacdHistogram, MacdIndicator, Mama, MaxDrawdown, McGinleyDynamic, MedianAbsoluteDeviation, MedianMa, MidPoint, Mom, OmegaRatio, PainIndex, PearsonCorrelation, PercentageTrailingStop, Pmo, PolarizedFractalEfficiency, Ppo, PpoHistogram, ProfitFactor, Qqe, RSquared, RealizedVolatility, RecoveryFactor, RegimeLabel, RenkoTrailingStop, Rmi, Roc, Rocp, Rocr, Rocr100, RollingIqr, RollingPercentileRank, RollingQuantile, RoofingFilter, Rsi, Rsx, RviVolatility, SharpeRatio, SineWave, SineWeightedMa, Skewness, Sma, Smma, SortinoRatio, SpearmanCorrelation, StandardError, StandardErrorBands, Stc, StdDev, StepTrailingStop, StochRsi, SuperSmoother, Tema, Tii, TrendLabel, TrendStrengthIndex, Trima, Trix, Tsf, TsfOscillator, Tsi, UlcerIndex, ValueAtRisk, Variance, VerticalHorizontalFilter, Vidya, WavePm, WinRate, Wma, ZScore, ZeroLagMacd, Zlema, T3};
|
||||
use wickra_core::{AdaptiveCycle, AdaptiveLaguerreFilter, Alma, AnchoredRsi, Apo, Autocorrelation, AverageDrawdown, BatchExt, Beta, BipowerVariation, BollingerBands, CalmarRatio, CenterOfGravity, Cfo, Cmo, CoefficientOfVariation, ConditionalValueAtRisk, ConnorsRsi, Coppock, CyberneticCycle, Decycler, DecyclerOscillator, Dema, DerivativeOscillator, DetrendedStdDev, DisparityIndex, DoubleBollinger, Dpo, DrawdownDuration, DynamicMomentumIndex, EhlersStochastic, Ehma, ElderImpulse, Ema, EmpiricalModeDecomposition, EwmaVolatility, Expectancy, Fama, FisherRsi, FisherTransform, Frama, GainLossRatio, Garch11, GeneralizedDema, GeometricMa, HilbertDominantCycle, HistoricalVolatility, Hma, HoltWinters, HtDcPhase, HtPhasor, HtTrendMode, HurstExponent, Indicator, InstantaneousTrendline, InverseFisherTransform, Jma, JumpIndicator, Kama, KellyCriterion, Kst, Kurtosis, LaguerreRsi, LinRegAngle, LinRegChannel, LinRegIntercept, LinRegSlope, LinearRegression, LogReturn, MaEnvelope, MaType, MacdExt, MacdFix, MacdHistogram, MacdIndicator, Mama, MaxDrawdown, McGinleyDynamic, MedianAbsoluteDeviation, MedianMa, MidPoint, Mom, OmegaRatio, PainIndex, PearsonCorrelation, PercentageTrailingStop, Pmo, PolarizedFractalEfficiency, Ppo, PpoHistogram, ProfitFactor, Qqe, RSquared, RealizedVolatility, RecoveryFactor, RegimeLabel, RenkoTrailingStop, Rmi, Roc, Rocp, Rocr, Rocr100, RollingIqr, RollingPercentileRank, RollingQuantile, RoofingFilter, Rsi, Rsx, RviVolatility, SharpeRatio, SineWave, SineWeightedMa, Skewness, Sma, Smma, SortinoRatio, SpearmanCorrelation, StandardError, StandardErrorBands, Stc, StdDev, StepTrailingStop, StochRsi, SuperSmoother, Tema, Tii, TrendLabel, TrendStrengthIndex, Trima, Trix, Tsf, TsfOscillator, Tsi, UlcerIndex, ValueAtRisk, Variance, VerticalHorizontalFilter, Vidya, VolatilityOfVolatility, WavePm, WinRate, Wma, ZScore, ZeroLagMacd, Zlema, T3};
|
||||
|
||||
/// Drive a single streaming + batch run through one scalar indicator. Marked
|
||||
/// `#[inline(never)]` so a panic backtrace pin-points the specific indicator.
|
||||
@@ -115,6 +115,10 @@ fuzz_target!(|data: Vec<f64>| {
|
||||
drive(|| HurstExponent::new(16, 4).unwrap(), &data);
|
||||
drive(|| LogReturn::new(1).unwrap(), &data);
|
||||
drive(|| RealizedVolatility::new(20).unwrap(), &data);
|
||||
drive(|| EwmaVolatility::new(0.94).unwrap(), &data);
|
||||
drive(|| Garch11::new(0.000_002, 0.1, 0.88).unwrap(), &data);
|
||||
drive(|| BipowerVariation::new(20).unwrap(), &data);
|
||||
drive(|| VolatilityOfVolatility::new(20, 20).unwrap(), &data);
|
||||
drive(|| RollingQuantile::new(20, 0.5).unwrap(), &data);
|
||||
drive(|| RollingIqr::new(14).unwrap(), &data);
|
||||
drive(|| RollingPercentileRank::new(14).unwrap(), &data);
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
//! WeightedClose.
|
||||
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
use wickra_core::{AbandonedBaby, Abcd, AccelerationBands, AcceleratorOscillator, AdOscillator, Adl, AdvanceBlock, Adx, Adxr, Alligator, AnchoredVwap, Aroon, AroonOscillator, Atr, AtrBands, AtrTrailingStop, AutoFib, AverageDailyRange, AwesomeOscillator, AwesomeOscillatorHistogram, BalanceOfPower, Bat, BatchExt, BeltHold, BodySizePct, Breakaway, Butterfly, Camarilla, Candle, Cci, ChaikinMoneyFlow, ChaikinOscillator, ChaikinVolatility, ChandeKrollStop, ChandelierExit, ChoppinessIndex, ClassicPivots, CloseVsOpen, ClosingMarubozu, ConcealingBabySwallow, Counterattack, Crab, CupAndHandle, Cypher, DayOfWeekProfile, DemandIndex, DemarkPivots, Doji, DojiStar, Donchian, DonchianStop, DoubleTopBottom, DownsideGapThreeMethods, DragonflyDoji, Dx, EaseOfMovement, ElderRay, Engulfing, EveningDojiStar, Evwma, FallingThreeMethods, FibArcs, FibChannel, FibConfluence, FibExtension, FibFan, FibProjection, FibRetracement, FibTimeZones, FibonacciPivots, FlagPennant, ForceIndex, FractalChaosBands, GapSideBySideWhite, GarmanKlassVolatility, Gartley, GatorOscillator, GoldenPocket, GravestoneDoji, Hammer, HangingMan, Harami, HeadAndShoulders, HeikinAshi, HiLoActivator, HighLowRange, HighWave, Hikkake, HikkakeModified, HomingPigeon, HurstChannel, Ichimoku, IdenticalThreeCrows, InNeck, Indicator, Inertia, InitialBalance, IntradayMomentumIndex, IntradayVolatilityProfile, InvertedHammer, KasePermissionStochastic, Keltner, Kicking, KickingByLength, Kvo, LadderBottom, LongLeggedDoji, LongLine, MarketFacilitationIndex, Marubozu, MassIndex, MatHold, MatchingLow, AvgPrice, MedianPrice, Mfi, MidPrice, MinusDi, MinusDm, MorningDojiStar, MorningEveningStar, Natr, Nvi, Obv, OnNeck, OpeningMarubozu, OpeningRange, OvernightGap, OvernightIntradayReturn, ParkinsonVolatility, Pgo, PiercingDarkCloud, PlusDi, PlusDm, Psar, Pvi, Qstick, RectangleRange, RickshawMan, RisingThreeMethods, RogersSatchellVolatility, RollingVwap, Rvi, Rwi, SarExt, SeasonalZScore, SeparatingLines, SessionHighLow, SessionRange, SessionVwap, Shark, ShootingStar, ShortLine, Smi, SpinningTop, StalledPattern, StarcBands, StickSandwich, Stochastic, StochasticCci, SuperTrend, Takuri, TasukiGap, TdCombo, TdCountdown, TdDeMarker, TdDifferential, TdLines, TdOpen, TdPressure, TdRangeProjection, TdRei, TdRiskLevel, TdSequential, TdSetup, ThreeDrives, ThreeInside, ThreeLineStrike, ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth, Thrusting, TimeOfDayReturnProfile, TpoProfile, Triangle, TripleTopBottom, TrueRange, Tsv, TtmSqueeze, TtmTrend, TurnOfMonth, Tweezer, TwoCrows, TypicalPrice, UltimateOscillator, UniqueThreeRiver, UpsideGapThreeMethods, UpsideGapTwoCrows, ValueArea, VoltyStop, VolumeByTimeProfile, VolumeOscillator, VolumePriceTrend, VolumeProfile, Vortex, Vwap, VwapStdDevBands, Vwma, Vzo, WaveTrend, Wedge, WeightedClose, WickRatio, WilliamsFractals, WilliamsR, WoodiePivots, YangZhangVolatility, YoyoExit, ZigZag};
|
||||
use wickra_core::{AbandonedBaby, Abcd, AccelerationBands, AcceleratorOscillator, AdOscillator, Adl, AdvanceBlock, Adx, Adxr, Alligator, AnchoredVwap, Aroon, AroonOscillator, Atr, AtrBands, AtrTrailingStop, AutoFib, AverageDailyRange, AwesomeOscillator, AwesomeOscillatorHistogram, BalanceOfPower, Bat, BatchExt, BeltHold, BodySizePct, Breakaway, Butterfly, Camarilla, Candle, Cci, ChaikinMoneyFlow, ChaikinOscillator, ChaikinVolatility, ChandeKrollStop, ChandelierExit, ChoppinessIndex, ClassicPivots, CloseVsOpen, ClosingMarubozu, ConcealingBabySwallow, Counterattack, Crab, CupAndHandle, Cypher, DayOfWeekProfile, DemandIndex, DemarkPivots, Doji, DojiStar, Donchian, DonchianStop, DoubleTopBottom, DownsideGapThreeMethods, DragonflyDoji, Dx, EaseOfMovement, ElderRay, Engulfing, EveningDojiStar, Evwma, FallingThreeMethods, FibArcs, FibChannel, FibConfluence, FibExtension, FibFan, FibProjection, FibRetracement, FibTimeZones, FibonacciPivots, FlagPennant, ForceIndex, FractalChaosBands, GapSideBySideWhite, GarmanKlassVolatility, Gartley, GatorOscillator, GoldenPocket, GravestoneDoji, Hammer, HangingMan, Harami, HeadAndShoulders, HeikinAshi, HiLoActivator, HighLowRange, HighWave, Hikkake, HikkakeModified, HomingPigeon, HurstChannel, Ichimoku, IdenticalThreeCrows, InNeck, Indicator, Inertia, InitialBalance, IntradayMomentumIndex, IntradayVolatilityProfile, InvertedHammer, KasePermissionStochastic, Keltner, Kicking, KickingByLength, Kvo, LadderBottom, LongLeggedDoji, LongLine, MarketFacilitationIndex, Marubozu, MassIndex, MatHold, MatchingLow, AvgPrice, MedianPrice, Mfi, MidPrice, MinusDi, MinusDm, MorningDojiStar, MorningEveningStar, Natr, Nvi, Obv, OnNeck, OpeningMarubozu, OpeningRange, OvernightGap, OvernightIntradayReturn, ParkinsonVolatility, Pgo, PiercingDarkCloud, PlusDi, PlusDm, Psar, Pvi, Qstick, RectangleRange, RickshawMan, RisingThreeMethods, RogersSatchellVolatility, RollingVwap, Rvi, Rwi, SarExt, SeasonalZScore, SeparatingLines, SessionHighLow, SessionRange, SessionVwap, Shark, ShootingStar, ShortLine, Smi, SpinningTop, StalledPattern, StarcBands, StickSandwich, Stochastic, StochasticCci, SuperTrend, Takuri, TasukiGap, TdCombo, TdCountdown, TdDeMarker, TdDifferential, TdLines, TdOpen, TdPressure, TdRangeProjection, TdRei, TdRiskLevel, TdSequential, TdSetup, ThreeDrives, ThreeInside, ThreeLineStrike, ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth, Thrusting, TimeOfDayReturnProfile, TpoProfile, Triangle, TripleTopBottom, TrueRange, Tsv, TtmSqueeze, TtmTrend, TurnOfMonth, Tweezer, TwoCrows, TypicalPrice, UltimateOscillator, UniqueThreeRiver, UpsideGapThreeMethods, UpsideGapTwoCrows, ValueArea, VolatilityCone, VolatilityRatio, VoltyStop, VolumeByTimeProfile, VolumeOscillator, VolumePriceTrend, VolumeProfile, Vortex, Vwap, VwapStdDevBands, Vwma, Vzo, WaveTrend, Wedge, WeightedClose, WickRatio, WilliamsFractals, WilliamsR, WoodiePivots, YangZhangVolatility, YoyoExit, ZigZag};
|
||||
|
||||
/// Convert a flat `f64` stream into a `Vec<Candle>` by chunking it into
|
||||
/// `[open, high, low, close, volume]` groups. Tuples that fail OHLCV
|
||||
@@ -62,6 +62,7 @@ fuzz_target!(|data: Vec<f64>| {
|
||||
}
|
||||
|
||||
// --- Volatility & ATR family ---
|
||||
drive(|| VolatilityRatio::new(14).unwrap(), &candles);
|
||||
drive(|| Atr::new(14).unwrap(), &candles);
|
||||
drive(|| Natr::new(14).unwrap(), &candles);
|
||||
drive(TrueRange::new, &candles);
|
||||
@@ -246,6 +247,7 @@ fuzz_target!(|data: Vec<f64>| {
|
||||
}
|
||||
|
||||
// --- Family 05: candle-input band/channel indicators (multi-output) ---
|
||||
drive(|| VolatilityCone::new(20, 60).unwrap(), &candles);
|
||||
{
|
||||
let mut ab = AccelerationBands::new(20, 0.001).unwrap();
|
||||
for c in &candles {
|
||||
|
||||
Reference in New Issue
Block a user