feat(indicators): B3 Trend & Directional batch (413 -> 420) (#181)
Adds the **B3 — Trend & Directional** batch: seven new indicators, taking the catalog from 413 to 420 (Trend & Directional family). | Indicator | Input → Output | Summary | |-----------|----------------|---------| | `Qstick` | candle → f64 | Chande's SMA of the candle body (close − open) | | `TtmTrend` | candle → f64 (±1) | John Carter close-vs-median-SMA trend filter | | `TrendStrengthIndex` | f64 → f64 | signed r² of an OLS regression of price vs time | | `PolarizedFractalEfficiency` | f64 → f64 | Hannula directional trend efficiency | | `WavePm` | f64 → f64 | Kase variance-normalised peak-momentum statistic (reconstruction) | | `GatorOscillator` | candle → struct | Bill Williams Alligator convergence/divergence histogram | | `KasePermissionStochastic` | candle → struct | double-smoothed stochastic permission filter | Note: the roadmap's "Directional Indicator +DI/−DI" item is already covered by the existing standalone `PlusDi` / `MinusDi` / `Dx`, so it is intentionally not re-added. All touchpoints wired: core (every-branch unit tests), Python/Node/WASM bindings, fuzz drivers, Python test registries + reference tests, Node factories, README/CHANGELOG counters. Local verify: `cargo test -p wickra-core` (lib 3389 + doc 378), `cargo clippy --workspace --all-targets --all-features -- -D warnings`, node build + 495 tests, maturin + 815 pytest, counter 420 == 420.
This commit is contained in:
@@ -6,6 +6,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
- **Qstick** — Qstick (Chande), the SMA of the candle body (close − open) as a net buying/selling pressure gauge (`QSTICK`).
|
||||
- **TTM Trend** — TTM Trend (John Carter), +1/−1 by whether the close sits above the SMA of recent median prices (`TTM_TREND`).
|
||||
- **Trend Strength Index** — trend strength index, the signed r² of a linear regression of price against time (`TREND_STRENGTH_INDEX`).
|
||||
- **Polarized Fractal Efficiency** — polarized fractal efficiency (Hannula), directional trend efficiency over a fractal lookback (`POLARIZED_FRACTAL_EFFICIENCY`).
|
||||
- **Wave PM** — Wave PM (Kase), a variance-normalised peak-momentum statistic (`WAVE_PM`).
|
||||
- **Gator Oscillator** — Gator Oscillator (Bill Williams), the Alligator convergence/divergence histogram (`GATOR_OSCILLATOR`).
|
||||
- **Kase Permission Stochastic** — Kase Permission Stochastic, a double-smoothed stochastic used as a trade-permission filter (`KASE_PERMISSION_STOCHASTIC`).
|
||||
|
||||
## [0.5.6] - 2026-06-04
|
||||
- **QQE** — quantitative qualitative estimation, a smoothed RSI with an ATR-of-RSI trailing line (`QQE`).
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<p align="center">
|
||||
<a href="https://wickra.org"><img src="https://raw.githubusercontent.com/wickra-lib/.github/main/profile/wickra-banner.webp?v=413" alt="Wickra — streaming-first technical indicators" width="100%"></a>
|
||||
<a href="https://wickra.org"><img src="https://raw.githubusercontent.com/wickra-lib/.github/main/profile/wickra-banner.webp?v=420" 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 413 indicators; start at the
|
||||
every one of the 420 indicators; start at the
|
||||
[indicators overview](https://docs.wickra.org/Indicators-Overview).
|
||||
- **Reference** — [warmup periods](https://docs.wickra.org/Warmup-Periods),
|
||||
[streaming vs batch](https://docs.wickra.org/Streaming-vs-Batch),
|
||||
@@ -136,7 +136,7 @@ python -m benchmarks.compare_libraries
|
||||
|
||||
## Indicators
|
||||
|
||||
413 streaming-first indicators across twenty-four families. Every one passes the
|
||||
420 streaming-first indicators across twenty-four families. Every one passes the
|
||||
`batch == streaming` equivalence test, reference-value tests, and reset
|
||||
semantics tests. Each has a per-indicator deep dive (formula, parameters,
|
||||
warmup) at [docs.wickra.org](https://docs.wickra.org/Indicators-Overview).
|
||||
@@ -145,7 +145,7 @@ warmup) at [docs.wickra.org](https://docs.wickra.org/Indicators-Overview).
|
||||
|--------|-----------|
|
||||
| Moving Averages | SMA, EMA, WMA, DEMA, TEMA, HMA, KAMA, SMMA, TRIMA, ZLEMA, T3, VWMA, ALMA, McGinley Dynamic, FRAMA, VIDYA, JMA, Alligator, EVWMA, SWMA, GMA, EHMA, Median MA, Adaptive Laguerre, GD, Holt-Winters |
|
||||
| Momentum Oscillators | RSI (Wilder), Anchored RSI, Stochastic, CCI, ROC, Williams %R, MFI, Awesome Oscillator, MOM, CMO, TSI, PMO, StochRSI, Ultimate Oscillator, RVI, PGO, KST, SMI, Laguerre RSI, Connors RSI, Inertia, ROC Percentage (ROCP), ROC Ratio (ROCR), ROC Ratio 100 (ROCR100), Disparity Index, Fisher RSI, RSX, Dynamic Momentum Index, Stochastic CCI, RMI, Derivative Oscillator, Elder Ray, Intraday Momentum Index, QQE |
|
||||
| Trend & Directional | MACD, MACD Fixed (MACDFIX), MACD Extended (MACDEXT), ADX (+DI/-DI), ADXR, Aroon, TRIX, Aroon Oscillator, Vortex, Random Walk Index, Trend Intensity Index, Wave Trend Oscillator, Mass Index, Choppiness Index, Vertical Horizontal Filter, Plus DM, Minus DM, Plus DI, Minus DI, DX |
|
||||
| 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 |
|
||||
| Volatility & Bands | ATR, Bollinger Bands, Keltner Channels, Donchian Channels, NATR, StdDev, Ulcer Index, Historical Volatility, Bollinger Bandwidth, %B, True Range, Chaikin Volatility, RVI (Relative Volatility Index), Parkinson Volatility, Garman-Klass Volatility, Rogers-Satchell Volatility, Yang-Zhang Volatility |
|
||||
| Bands & Channels | MA Envelope, Acceleration Bands, STARC Bands, ATR Bands, Hurst Channel, LinReg Channel, Standard Error Bands, Double Bollinger Bands, TTM Squeeze, Fractal Chaos Bands, VWAP StdDev Bands |
|
||||
@@ -245,7 +245,7 @@ A Python live-trading example using the public `websockets` package lives at
|
||||
```
|
||||
wickra/
|
||||
├── crates/
|
||||
│ ├── wickra-core/ core engine + all 413 indicators
|
||||
│ ├── wickra-core/ core engine + all 420 indicators
|
||||
│ ├── wickra/ top-level facade crate (publishes on crates.io) + benches/
|
||||
│ └── wickra-data/ CSV reader, tick aggregator, live exchange feeds
|
||||
├── bindings/
|
||||
|
||||
@@ -28,6 +28,9 @@ function num(v) {
|
||||
// --- Scalar indicators: update(value) vs batch(prices) ---
|
||||
|
||||
const scalarFactories = {
|
||||
WAVE_PM: () => new wickra.WAVE_PM(32, 3),
|
||||
POLARIZED_FRACTAL_EFFICIENCY: () => new wickra.POLARIZED_FRACTAL_EFFICIENCY(10, 5),
|
||||
TREND_STRENGTH_INDEX: () => new wickra.TREND_STRENGTH_INDEX(20),
|
||||
DerivativeOscillator: () => new wickra.DerivativeOscillator(14, 5, 3, 9),
|
||||
RMI: () => new wickra.RMI(14, 5),
|
||||
DynamicMomentumIndex: () => new wickra.DynamicMomentumIndex(14),
|
||||
@@ -342,6 +345,8 @@ const candleScalar = {
|
||||
HighLowRange: { make: () => new wickra.HighLowRange(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
StochasticCCI: { make: () => new wickra.StochasticCCI(14), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
|
||||
IMI: { make: () => new wickra.IMI(14), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
TTM_TREND: { make: () => new wickra.TTM_TREND(6), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
|
||||
Qstick: { make: () => new wickra.Qstick(10), step: (ind, i) => ind.update(open[i], close[i]), batch: (ind) => ind.batch(open, close) },
|
||||
};
|
||||
|
||||
for (const [name, d] of Object.entries(candleScalar)) {
|
||||
@@ -426,6 +431,8 @@ const multi = {
|
||||
FibTimeZones: { make: () => new wickra.FibTimeZones(), fields: ['onZone', 'barsToNext'], step: (ind, i) => ind.update(high[i], low[i]), batch: (ind) => ind.batch(high, low) },
|
||||
ElderRay: { make: () => new wickra.ElderRay(13), fields: ['bullPower', 'bearPower'], step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
|
||||
QQE: { make: () => new wickra.QQE(14, 5, 4.236), fields: ['rsiMa', 'trailingLine'], step: (ind, i) => ind.update(close[i]), batch: (ind) => ind.batch(close) },
|
||||
GatorOscillator: { make: () => new wickra.GatorOscillator(13, 8, 5), fields: ['upper', 'lower'], step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
|
||||
KasePermissionStochastic: { make: () => new wickra.KasePermissionStochastic(9, 3), fields: ['fast', 'slow'], step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
|
||||
};
|
||||
|
||||
for (const [name, d] of Object.entries(multi)) {
|
||||
|
||||
Vendored
+71
@@ -77,6 +77,14 @@ export interface ElderRayValue {
|
||||
bullPower: number
|
||||
bearPower: number
|
||||
}
|
||||
export interface GatorOscillatorValue {
|
||||
upper: number
|
||||
lower: number
|
||||
}
|
||||
export interface KasePermissionStochasticValue {
|
||||
fast: number
|
||||
slow: number
|
||||
}
|
||||
export interface StochValue {
|
||||
k: number
|
||||
d: number
|
||||
@@ -961,6 +969,15 @@ export declare class DynamicMomentumIndex {
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type TrendStrengthIndexNode = TREND_STRENGTH_INDEX
|
||||
export declare class TREND_STRENGTH_INDEX {
|
||||
constructor(period: number)
|
||||
update(value: number): number | null
|
||||
batch(prices: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type JumpIndicatorNode = JumpIndicator
|
||||
export declare class JumpIndicator {
|
||||
constructor(period: number, threshold: number)
|
||||
@@ -1494,6 +1511,60 @@ export declare class ElderRay {
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type TtmTrendNode = TTM_TREND
|
||||
export declare class TTM_TREND {
|
||||
constructor(period: number)
|
||||
update(high: number, low: number, close: number): number | null
|
||||
batch(high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type QstickNode = Qstick
|
||||
export declare class Qstick {
|
||||
constructor(period: number)
|
||||
update(open: number, close: number): number | null
|
||||
batch(open: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type PolarizedFractalEfficiencyNode = POLARIZED_FRACTAL_EFFICIENCY
|
||||
export declare class POLARIZED_FRACTAL_EFFICIENCY {
|
||||
constructor(period: number, smoothing: number)
|
||||
update(value: number): number | null
|
||||
batch(prices: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type WavePmNode = WAVE_PM
|
||||
export declare class WAVE_PM {
|
||||
constructor(length: number, smoothing: number)
|
||||
update(value: number): number | null
|
||||
batch(prices: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type GatorOscillatorNode = GatorOscillator
|
||||
export declare class GatorOscillator {
|
||||
constructor(jawPeriod: number, teethPeriod: number, lipsPeriod: number)
|
||||
update(high: number, low: number, close: number): GatorOscillatorValue | null
|
||||
batch(high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type KasePermissionStochasticNode = KasePermissionStochastic
|
||||
export declare class KasePermissionStochastic {
|
||||
constructor(length: number, smooth: number)
|
||||
update(high: number, low: number, close: number): KasePermissionStochasticValue | null
|
||||
batch(high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type StochNode = Stochastic
|
||||
export declare class Stochastic {
|
||||
constructor(kPeriod: number, dPeriod: number)
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -214,6 +214,11 @@ node_scalar_indicator!(
|
||||
"DynamicMomentumIndex",
|
||||
wc::DynamicMomentumIndex
|
||||
);
|
||||
node_scalar_indicator!(
|
||||
TrendStrengthIndexNode,
|
||||
"TREND_STRENGTH_INDEX",
|
||||
wc::TrendStrengthIndex
|
||||
);
|
||||
#[napi(js_name = "JumpIndicator")]
|
||||
pub struct JumpIndicatorNode {
|
||||
inner: wc::JumpIndicator,
|
||||
@@ -2295,6 +2300,328 @@ impl ElderRayNode {
|
||||
}
|
||||
}
|
||||
|
||||
#[napi(js_name = "TTM_TREND")]
|
||||
pub struct TtmTrendNode {
|
||||
inner: wc::TtmTrend,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl TtmTrendNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(period: u32) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::TtmTrend::new(period as usize).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(&mut self, high: f64, low: f64, close: f64) -> napi::Result<Option<f64>> {
|
||||
Ok(self.inner.update(cnd(high, low, close, 0.0)?))
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
high: Vec<f64>,
|
||||
low: Vec<f64>,
|
||||
close: Vec<f64>,
|
||||
) -> napi::Result<Vec<f64>> {
|
||||
if high.len() != low.len() || low.len() != close.len() {
|
||||
return Err(NapiError::from_reason(
|
||||
"high, low, close must be equal length".to_string(),
|
||||
));
|
||||
}
|
||||
let mut out = Vec::with_capacity(high.len());
|
||||
for i in 0..high.len() {
|
||||
out.push(
|
||||
self.inner
|
||||
.update(cnd(high[i], low[i], close[i], 0.0)?)
|
||||
.unwrap_or(f64::NAN),
|
||||
);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
#[napi]
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
#[napi(js_name = "isReady")]
|
||||
pub fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
#[napi(js_name = "warmupPeriod")]
|
||||
pub fn warmup_period(&self) -> u32 {
|
||||
self.inner.warmup_period() as u32
|
||||
}
|
||||
}
|
||||
|
||||
#[napi(js_name = "Qstick")]
|
||||
pub struct QstickNode {
|
||||
inner: wc::Qstick,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl QstickNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(period: u32) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::Qstick::new(period as usize).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(&mut self, open: f64, close: f64) -> napi::Result<Option<f64>> {
|
||||
let hi = open.max(close);
|
||||
let lo = open.min(close);
|
||||
Ok(self.inner.update(cnd4(open, hi, lo, close)?))
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(&mut self, open: Vec<f64>, close: Vec<f64>) -> napi::Result<Vec<f64>> {
|
||||
if open.len() != close.len() {
|
||||
return Err(NapiError::from_reason(
|
||||
"open, close must be equal length".to_string(),
|
||||
));
|
||||
}
|
||||
let mut out = Vec::with_capacity(open.len());
|
||||
for i in 0..open.len() {
|
||||
let hi = open[i].max(close[i]);
|
||||
let lo = open[i].min(close[i]);
|
||||
out.push(
|
||||
self.inner
|
||||
.update(cnd4(open[i], hi, lo, close[i])?)
|
||||
.unwrap_or(f64::NAN),
|
||||
);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
#[napi]
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
#[napi(js_name = "isReady")]
|
||||
pub fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
#[napi(js_name = "warmupPeriod")]
|
||||
pub fn warmup_period(&self) -> u32 {
|
||||
self.inner.warmup_period() as u32
|
||||
}
|
||||
}
|
||||
|
||||
#[napi(js_name = "POLARIZED_FRACTAL_EFFICIENCY")]
|
||||
pub struct PolarizedFractalEfficiencyNode {
|
||||
inner: wc::PolarizedFractalEfficiency,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl PolarizedFractalEfficiencyNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(period: u32, smoothing: u32) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::PolarizedFractalEfficiency::new(period as usize, smoothing as usize)
|
||||
.map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(&mut self, value: f64) -> Option<f64> {
|
||||
self.inner.update(value)
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(&mut self, prices: Vec<f64>) -> Vec<f64> {
|
||||
flatten(self.inner.batch(&prices))
|
||||
}
|
||||
#[napi]
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
#[napi(js_name = "isReady")]
|
||||
pub fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
#[napi(js_name = "warmupPeriod")]
|
||||
pub fn warmup_period(&self) -> u32 {
|
||||
self.inner.warmup_period() as u32
|
||||
}
|
||||
}
|
||||
|
||||
#[napi(js_name = "WAVE_PM")]
|
||||
pub struct WavePmNode {
|
||||
inner: wc::WavePm,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl WavePmNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(length: u32, smoothing: u32) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::WavePm::new(length as usize, smoothing as usize).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(&mut self, value: f64) -> Option<f64> {
|
||||
self.inner.update(value)
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(&mut self, prices: Vec<f64>) -> Vec<f64> {
|
||||
flatten(self.inner.batch(&prices))
|
||||
}
|
||||
#[napi]
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
#[napi(js_name = "isReady")]
|
||||
pub fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
#[napi(js_name = "warmupPeriod")]
|
||||
pub fn warmup_period(&self) -> u32 {
|
||||
self.inner.warmup_period() as u32
|
||||
}
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
pub struct GatorOscillatorValue {
|
||||
pub upper: f64,
|
||||
pub lower: f64,
|
||||
}
|
||||
|
||||
#[napi(js_name = "GatorOscillator")]
|
||||
pub struct GatorOscillatorNode {
|
||||
inner: wc::GatorOscillator,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl GatorOscillatorNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(jaw_period: u32, teeth_period: u32, lips_period: u32) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::GatorOscillator::new(
|
||||
jaw_period as usize,
|
||||
teeth_period as usize,
|
||||
lips_period as usize,
|
||||
)
|
||||
.map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(
|
||||
&mut self,
|
||||
high: f64,
|
||||
low: f64,
|
||||
close: f64,
|
||||
) -> napi::Result<Option<GatorOscillatorValue>> {
|
||||
Ok(self
|
||||
.inner
|
||||
.update(cnd(high, low, close, 0.0)?)
|
||||
.map(|o| GatorOscillatorValue {
|
||||
upper: o.upper,
|
||||
lower: o.lower,
|
||||
}))
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
high: Vec<f64>,
|
||||
low: Vec<f64>,
|
||||
close: Vec<f64>,
|
||||
) -> napi::Result<Vec<f64>> {
|
||||
if high.len() != low.len() || low.len() != close.len() {
|
||||
return Err(NapiError::from_reason(
|
||||
"high, low, close must be equal length".to_string(),
|
||||
));
|
||||
}
|
||||
let n = high.len();
|
||||
let mut out = vec![f64::NAN; n * 2];
|
||||
for i in 0..n {
|
||||
if let Some(o) = self.inner.update(cnd(high[i], low[i], close[i], 0.0)?) {
|
||||
out[i * 2] = o.upper;
|
||||
out[i * 2 + 1] = o.lower;
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
#[napi]
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
#[napi(js_name = "isReady")]
|
||||
pub fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
#[napi(js_name = "warmupPeriod")]
|
||||
pub fn warmup_period(&self) -> u32 {
|
||||
self.inner.warmup_period() as u32
|
||||
}
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
pub struct KasePermissionStochasticValue {
|
||||
pub fast: f64,
|
||||
pub slow: f64,
|
||||
}
|
||||
|
||||
#[napi(js_name = "KasePermissionStochastic")]
|
||||
pub struct KasePermissionStochasticNode {
|
||||
inner: wc::KasePermissionStochastic,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl KasePermissionStochasticNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(length: u32, smooth: u32) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::KasePermissionStochastic::new(length as usize, smooth as usize)
|
||||
.map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(
|
||||
&mut self,
|
||||
high: f64,
|
||||
low: f64,
|
||||
close: f64,
|
||||
) -> napi::Result<Option<KasePermissionStochasticValue>> {
|
||||
Ok(self
|
||||
.inner
|
||||
.update(cnd(high, low, close, 0.0)?)
|
||||
.map(|o| KasePermissionStochasticValue {
|
||||
fast: o.fast,
|
||||
slow: o.slow,
|
||||
}))
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
high: Vec<f64>,
|
||||
low: Vec<f64>,
|
||||
close: Vec<f64>,
|
||||
) -> napi::Result<Vec<f64>> {
|
||||
if high.len() != low.len() || low.len() != close.len() {
|
||||
return Err(NapiError::from_reason(
|
||||
"high, low, close must be equal length".to_string(),
|
||||
));
|
||||
}
|
||||
let n = high.len();
|
||||
let mut out = vec![f64::NAN; n * 2];
|
||||
for i in 0..n {
|
||||
if let Some(o) = self.inner.update(cnd(high[i], low[i], close[i], 0.0)?) {
|
||||
out[i * 2] = o.fast;
|
||||
out[i * 2 + 1] = o.slow;
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
#[napi]
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
#[napi(js_name = "isReady")]
|
||||
pub fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
#[napi(js_name = "warmupPeriod")]
|
||||
pub fn warmup_period(&self) -> u32 {
|
||||
self.inner.warmup_period() as u32
|
||||
}
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
pub struct StochValue {
|
||||
pub k: f64,
|
||||
|
||||
@@ -25,6 +25,13 @@ from __future__ import annotations
|
||||
|
||||
from ._wickra import (
|
||||
__version__,
|
||||
Qstick,
|
||||
GatorOscillator,
|
||||
KasePermissionStochastic,
|
||||
WAVE_PM,
|
||||
POLARIZED_FRACTAL_EFFICIENCY,
|
||||
TREND_STRENGTH_INDEX,
|
||||
TTM_TREND,
|
||||
QQE,
|
||||
IMI,
|
||||
ElderRay,
|
||||
@@ -466,6 +473,13 @@ from ._wickra import (
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"Qstick",
|
||||
"GatorOscillator",
|
||||
"KasePermissionStochastic",
|
||||
"WAVE_PM",
|
||||
"POLARIZED_FRACTAL_EFFICIENCY",
|
||||
"TREND_STRENGTH_INDEX",
|
||||
"TTM_TREND",
|
||||
"QQE",
|
||||
"IMI",
|
||||
"ElderRay",
|
||||
|
||||
@@ -2866,6 +2866,435 @@ impl PyStochasticCci {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== TtmTrend ==============================
|
||||
|
||||
#[pyclass(name = "TTM_TREND", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyTtmTrend {
|
||||
inner: wc::TtmTrend,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyTtmTrend {
|
||||
#[new]
|
||||
#[pyo3(signature = (period=6))]
|
||||
fn new(period: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::TtmTrend::new(period).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult<Option<f64>> {
|
||||
let c = extract_candle(candle)?;
|
||||
Ok(self.inner.update(c))
|
||||
}
|
||||
/// Batch over numpy columns: high, low, close (all 1-D, equal length).
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
let h = high
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let l = low
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let c = close
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
if h.len() != l.len() || l.len() != c.len() {
|
||||
return Err(PyValueError::new_err(
|
||||
"high, low, close must be equal length",
|
||||
));
|
||||
}
|
||||
let mut out = Vec::with_capacity(h.len());
|
||||
for i in 0..h.len() {
|
||||
let candle = wc::Candle::new(c[i], h[i], l[i], c[i], 0.0, 0).map_err(map_err)?;
|
||||
out.push(self.inner.update(candle).unwrap_or(f64::NAN));
|
||||
}
|
||||
Ok(out.into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn period(&self) -> usize {
|
||||
self.inner.period()
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.inner.warmup_period()
|
||||
}
|
||||
fn __repr__(&self) -> String {
|
||||
format!("TTM_TREND(period={})", self.inner.period())
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== TrendStrengthIndex ==============================
|
||||
|
||||
#[pyclass(
|
||||
name = "TREND_STRENGTH_INDEX",
|
||||
module = "wickra._wickra",
|
||||
skip_from_py_object
|
||||
)]
|
||||
#[derive(Clone)]
|
||||
struct PyTrendStrengthIndex {
|
||||
inner: wc::TrendStrengthIndex,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyTrendStrengthIndex {
|
||||
#[new]
|
||||
#[pyo3(signature = (period=20))]
|
||||
fn new(period: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::TrendStrengthIndex::new(period).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
fn update(&mut self, value: f64) -> Option<f64> {
|
||||
self.inner.update(value)
|
||||
}
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
prices: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
let s = prices
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
Ok(flatten(self.inner.batch(s)).into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn period(&self) -> usize {
|
||||
self.inner.period()
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.inner.warmup_period()
|
||||
}
|
||||
fn __repr__(&self) -> String {
|
||||
format!("TREND_STRENGTH_INDEX(period={})", self.inner.period())
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Qstick ==============================
|
||||
|
||||
#[pyclass(name = "Qstick", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyQstick {
|
||||
inner: wc::Qstick,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyQstick {
|
||||
#[new]
|
||||
#[pyo3(signature = (period=10))]
|
||||
fn new(period: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::Qstick::new(period).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult<Option<f64>> {
|
||||
let c = extract_candle(candle)?;
|
||||
Ok(self.inner.update(c))
|
||||
}
|
||||
/// Batch over open/close numpy columns (Qstick reads the body close-open).
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
open: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
let o = open
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let c = close
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
if o.len() != c.len() {
|
||||
return Err(PyValueError::new_err("open, close must be equal length"));
|
||||
}
|
||||
let n = o.len();
|
||||
let mut out = Vec::with_capacity(n);
|
||||
for i in 0..n {
|
||||
let hi = o[i].max(c[i]);
|
||||
let lo = o[i].min(c[i]);
|
||||
let candle = wc::Candle::new(o[i], hi, lo, c[i], 0.0, 0).map_err(map_err)?;
|
||||
out.push(self.inner.update(candle).unwrap_or(f64::NAN));
|
||||
}
|
||||
Ok(out.into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn period(&self) -> usize {
|
||||
self.inner.period()
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.inner.warmup_period()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== PolarizedFractalEfficiency ==============================
|
||||
|
||||
#[pyclass(
|
||||
name = "POLARIZED_FRACTAL_EFFICIENCY",
|
||||
module = "wickra._wickra",
|
||||
skip_from_py_object
|
||||
)]
|
||||
#[derive(Clone)]
|
||||
struct PyPolarizedFractalEfficiency {
|
||||
inner: wc::PolarizedFractalEfficiency,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyPolarizedFractalEfficiency {
|
||||
#[new]
|
||||
#[pyo3(signature = (period=10, smoothing=5))]
|
||||
fn new(period: usize, smoothing: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::PolarizedFractalEfficiency::new(period, smoothing).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
fn update(&mut self, value: f64) -> Option<f64> {
|
||||
self.inner.update(value)
|
||||
}
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
prices: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
let s = prices
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
Ok(flatten(self.inner.batch(s)).into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn period(&self) -> usize {
|
||||
self.inner.periods().0
|
||||
}
|
||||
#[getter]
|
||||
fn smoothing(&self) -> usize {
|
||||
self.inner.periods().1
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.inner.warmup_period()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== WavePm ==============================
|
||||
|
||||
#[pyclass(name = "WAVE_PM", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyWavePm {
|
||||
inner: wc::WavePm,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyWavePm {
|
||||
#[new]
|
||||
#[pyo3(signature = (length=32, smoothing=3))]
|
||||
fn new(length: usize, smoothing: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::WavePm::new(length, smoothing).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
fn update(&mut self, value: f64) -> Option<f64> {
|
||||
self.inner.update(value)
|
||||
}
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
prices: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
let s = prices
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
Ok(flatten(self.inner.batch(s)).into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn length(&self) -> usize {
|
||||
self.inner.periods().0
|
||||
}
|
||||
#[getter]
|
||||
fn smoothing(&self) -> usize {
|
||||
self.inner.periods().1
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.inner.warmup_period()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== GatorOscillator ==============================
|
||||
|
||||
#[pyclass(
|
||||
name = "GatorOscillator",
|
||||
module = "wickra._wickra",
|
||||
skip_from_py_object
|
||||
)]
|
||||
#[derive(Clone)]
|
||||
struct PyGatorOscillator {
|
||||
inner: wc::GatorOscillator,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyGatorOscillator {
|
||||
#[new]
|
||||
#[pyo3(signature = (jaw_period=13, teeth_period=8, lips_period=5))]
|
||||
fn new(jaw_period: usize, teeth_period: usize, lips_period: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::GatorOscillator::new(jaw_period, teeth_period, lips_period)
|
||||
.map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult<Option<(f64, f64)>> {
|
||||
let c = extract_candle(candle)?;
|
||||
Ok(self.inner.update(c).map(|o| (o.upper, o.lower)))
|
||||
}
|
||||
/// Batch over high/low/close numpy columns. Returns shape `(n, 2)` for
|
||||
/// `[upper, lower]`.
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray2<f64>>> {
|
||||
let h = high
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let l = low
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let c = close
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
if h.len() != l.len() || l.len() != c.len() {
|
||||
return Err(PyValueError::new_err(
|
||||
"high, low, close must be equal length",
|
||||
));
|
||||
}
|
||||
let n = h.len();
|
||||
let mut out = vec![f64::NAN; n * 2];
|
||||
for i in 0..n {
|
||||
let candle = wc::Candle::new(c[i], h[i], l[i], c[i], 0.0, 0).map_err(map_err)?;
|
||||
if let Some(o) = self.inner.update(candle) {
|
||||
out[i * 2] = o.upper;
|
||||
out[i * 2 + 1] = o.lower;
|
||||
}
|
||||
}
|
||||
Ok(numpy::ndarray::Array2::from_shape_vec((n, 2), out)
|
||||
.expect("shape consistent")
|
||||
.into_pyarray(py))
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.inner.warmup_period()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== KasePermissionStochastic ==============================
|
||||
|
||||
#[pyclass(
|
||||
name = "KasePermissionStochastic",
|
||||
module = "wickra._wickra",
|
||||
skip_from_py_object
|
||||
)]
|
||||
#[derive(Clone)]
|
||||
struct PyKasePermissionStochastic {
|
||||
inner: wc::KasePermissionStochastic,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyKasePermissionStochastic {
|
||||
#[new]
|
||||
#[pyo3(signature = (length=9, smooth=3))]
|
||||
fn new(length: usize, smooth: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::KasePermissionStochastic::new(length, smooth).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult<Option<(f64, f64)>> {
|
||||
let c = extract_candle(candle)?;
|
||||
Ok(self.inner.update(c).map(|o| (o.fast, o.slow)))
|
||||
}
|
||||
/// Batch over high/low/close numpy columns. Returns shape `(n, 2)` for
|
||||
/// `[fast, slow]`.
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray2<f64>>> {
|
||||
let h = high
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let l = low
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let c = close
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
if h.len() != l.len() || l.len() != c.len() {
|
||||
return Err(PyValueError::new_err(
|
||||
"high, low, close must be equal length",
|
||||
));
|
||||
}
|
||||
let n = h.len();
|
||||
let mut out = vec![f64::NAN; n * 2];
|
||||
for i in 0..n {
|
||||
let candle = wc::Candle::new(c[i], h[i], l[i], c[i], 0.0, 0).map_err(map_err)?;
|
||||
if let Some(o) = self.inner.update(candle) {
|
||||
out[i * 2] = o.fast;
|
||||
out[i * 2 + 1] = o.slow;
|
||||
}
|
||||
}
|
||||
Ok(numpy::ndarray::Array2::from_shape_vec((n, 2), out)
|
||||
.expect("shape consistent")
|
||||
.into_pyarray(py))
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.inner.warmup_period()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Stochastic ==============================
|
||||
|
||||
#[pyclass(name = "IMI", module = "wickra._wickra", skip_from_py_object)]
|
||||
@@ -20986,5 +21415,12 @@ fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_class::<PyRsx>()?;
|
||||
m.add_class::<PyDynamicMomentumIndex>()?;
|
||||
m.add_class::<PyStochasticCci>()?;
|
||||
m.add_class::<PyTtmTrend>()?;
|
||||
m.add_class::<PyTrendStrengthIndex>()?;
|
||||
m.add_class::<PyQstick>()?;
|
||||
m.add_class::<PyPolarizedFractalEfficiency>()?;
|
||||
m.add_class::<PyWavePm>()?;
|
||||
m.add_class::<PyGatorOscillator>()?;
|
||||
m.add_class::<PyKasePermissionStochastic>()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -45,6 +45,9 @@ def ohlcv() -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
|
||||
# --- Scalar (f64 -> f64) indicators ---------------------------------------
|
||||
|
||||
SCALAR = [
|
||||
(ta.WAVE_PM, (32, 3)),
|
||||
(ta.POLARIZED_FRACTAL_EFFICIENCY, (10, 5)),
|
||||
(ta.TREND_STRENGTH_INDEX, (20,)),
|
||||
(ta.DerivativeOscillator, (14, 5, 3, 9)),
|
||||
(ta.RMI, (14, 5)),
|
||||
(ta.DynamicMomentumIndex, (14,)),
|
||||
@@ -355,6 +358,7 @@ def test_relative_strength_streaming_matches_batch():
|
||||
# 6-tuple candle; the batch helper takes only the columns it needs.
|
||||
|
||||
CANDLE_SCALAR = {
|
||||
"TTM_TREND": (lambda: ta.TTM_TREND(6), lambda ind, h, l, c, v: ind.batch(h, l, c)),
|
||||
"StochasticCCI": (lambda: ta.StochasticCCI(14), lambda ind, h, l, c, v: ind.batch(h, l, c)),
|
||||
# Per-bar OHLC transforms (open matters). The streaming harness feeds
|
||||
# open == close, so batch passes the close column in for open to match.
|
||||
@@ -892,6 +896,16 @@ def test_candle_scalar_streaming_matches_batch(name, ohlcv):
|
||||
# --- Candle-input, multi-output indicators --------------------------------
|
||||
|
||||
MULTI = {
|
||||
"KasePermissionStochastic": (
|
||||
lambda: ta.KasePermissionStochastic(9, 3),
|
||||
lambda ind, h, l, c, v: ind.batch(h, l, c),
|
||||
2,
|
||||
),
|
||||
"GatorOscillator": (
|
||||
lambda: ta.GatorOscillator(13, 8, 5),
|
||||
lambda ind, h, l, c, v: ind.batch(h, l, c),
|
||||
2,
|
||||
),
|
||||
"ElderRay": (
|
||||
lambda: ta.ElderRay(13),
|
||||
lambda ind, h, l, c, v: ind.batch(h, l, c),
|
||||
@@ -2779,6 +2793,75 @@ def test_imi_reference():
|
||||
assert math.isnan(out[1])
|
||||
assert out[2] == pytest.approx(75.0)
|
||||
|
||||
|
||||
def test_qstick_reference():
|
||||
q = ta.Qstick(3)
|
||||
open_ = np.array([10.0, 10.0, 10.0])
|
||||
close = np.array([11.0, 11.0, 11.0])
|
||||
out = q.batch(open_, close)
|
||||
# Each body is close - open = 1; SMA(3) of [1, 1, 1] = 1.
|
||||
assert math.isnan(out[0])
|
||||
assert math.isnan(out[1])
|
||||
assert out[2] == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_ttm_trend_reference():
|
||||
t = ta.TTM_TREND(3)
|
||||
high = np.array([13.0, 13.0, 13.0])
|
||||
low = np.array([9.0, 9.0, 9.0])
|
||||
close = np.array([12.0, 12.0, 12.0])
|
||||
out = t.batch(high, low, close)
|
||||
# Median (13 + 9) / 2 = 11; close 12 is above the SMA(3) reference -> +1.
|
||||
assert math.isnan(out[0])
|
||||
assert out[2] == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_trend_strength_index_reference():
|
||||
tsi = ta.TREND_STRENGTH_INDEX(10)
|
||||
closes = np.arange(10, dtype=float)
|
||||
out = tsi.batch(closes)
|
||||
# A clean ramp is a perfect uptrend -> signed r^2 = +1.
|
||||
assert math.isclose(out[-1], 1.0, abs_tol=1e-9)
|
||||
|
||||
|
||||
def test_polarized_fractal_efficiency_reference():
|
||||
pfe = ta.POLARIZED_FRACTAL_EFFICIENCY(5, 3)
|
||||
closes = np.arange(20, dtype=float)
|
||||
out = pfe.batch(closes)
|
||||
# On a straight ramp the path equals the diagonal -> efficiency 1 -> +100.
|
||||
assert math.isclose(out[-1], 100.0, abs_tol=1e-9)
|
||||
|
||||
|
||||
def test_wave_pm_reference():
|
||||
wpm = ta.WAVE_PM(10, 3)
|
||||
closes = np.arange(60, dtype=float) * 5.0
|
||||
out = wpm.batch(closes)
|
||||
# Constant-slope ramp: momentum equals its energy -> 100 * (1 - e^-0.5).
|
||||
baseline = 100.0 * (1.0 - math.exp(-0.5))
|
||||
assert math.isclose(out[-1], baseline, abs_tol=1e-9)
|
||||
|
||||
|
||||
def test_gator_oscillator_reference():
|
||||
g = ta.GatorOscillator(13, 8, 5)
|
||||
n = 40
|
||||
high = np.full(n, 11.0)
|
||||
low = np.full(n, 9.0)
|
||||
close = np.full(n, 10.0)
|
||||
out = g.batch(high, low, close)
|
||||
# Constant median collapses all three Alligator lines -> both bars zero.
|
||||
assert out[-1][0] == pytest.approx(0.0)
|
||||
assert out[-1][1] == pytest.approx(0.0)
|
||||
|
||||
|
||||
def test_kase_permission_stochastic_reference():
|
||||
k = ta.KasePermissionStochastic(4, 2)
|
||||
n = 20
|
||||
flat = np.full(n, 10.0)
|
||||
out = k.batch(flat, flat, flat)
|
||||
# HH == LL -> raw %K defaults to the neutral 50 -> both lines at 50.
|
||||
assert out[-1][0] == pytest.approx(50.0)
|
||||
assert out[-1][1] == pytest.approx(50.0)
|
||||
|
||||
# --- Lifecycle ------------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
@@ -80,6 +80,14 @@ wasm_scalar_indicator!(WasmTrima, "TRIMA", wc::Trima, period: usize);
|
||||
wasm_scalar_indicator!(WasmZlema, "ZLEMA", wc::Zlema, period: usize);
|
||||
wasm_scalar_indicator!(WasmT3, "T3", wc::T3, period: usize, v: f64);
|
||||
wasm_scalar_indicator!(WasmAlma, "ALMA", wc::Alma, period: usize, offset: f64, sigma: f64);
|
||||
wasm_scalar_indicator!(
|
||||
WasmPolarizedFractalEfficiency,
|
||||
"POLARIZED_FRACTAL_EFFICIENCY",
|
||||
wc::PolarizedFractalEfficiency,
|
||||
period: usize,
|
||||
smoothing: usize
|
||||
);
|
||||
wasm_scalar_indicator!(WasmWavePm, "WAVE_PM", wc::WavePm, length: usize, smoothing: usize);
|
||||
wasm_scalar_indicator!(WasmMcGinleyDynamic, "McGinleyDynamic", wc::McGinleyDynamic, period: usize);
|
||||
wasm_scalar_indicator!(WasmFrama, "FRAMA", wc::Frama, period: usize);
|
||||
wasm_scalar_indicator!(WasmVidya, "VIDYA", wc::Vidya, period: usize, cmo_period: usize);
|
||||
@@ -2255,6 +2263,219 @@ impl WasmElderRay {
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = TTM_TREND)]
|
||||
pub struct WasmTtmTrend {
|
||||
inner: wc::TtmTrend,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = TTM_TREND)]
|
||||
impl WasmTtmTrend {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(period: usize) -> Result<WasmTtmTrend, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::TtmTrend::new(period).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
pub fn update(&mut self, high: f64, low: f64, close: f64) -> Result<Option<f64>, JsError> {
|
||||
let c = make_candle(high, low, close, 0.0)?;
|
||||
Ok(self.inner.update(c))
|
||||
}
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
high: &[f64],
|
||||
low: &[f64],
|
||||
close: &[f64],
|
||||
) -> Result<Float64Array, JsError> {
|
||||
if high.len() != low.len() || low.len() != close.len() {
|
||||
return Err(JsError::new("high, low, close must be equal length"));
|
||||
}
|
||||
let mut out = Vec::with_capacity(high.len());
|
||||
for i in 0..high.len() {
|
||||
let c = make_candle(high[i], low[i], close[i], 0.0)?;
|
||||
out.push(self.inner.update(c).unwrap_or(f64::NAN));
|
||||
}
|
||||
Ok(Float64Array::from(out.as_slice()))
|
||||
}
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = Qstick)]
|
||||
pub struct WasmQstick {
|
||||
inner: wc::Qstick,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = Qstick)]
|
||||
impl WasmQstick {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(period: usize) -> Result<WasmQstick, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::Qstick::new(period).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
/// Batch over open/close arrays; `NaN` during warmup.
|
||||
pub fn batch(&mut self, open: &[f64], close: &[f64]) -> Result<Float64Array, JsError> {
|
||||
let n = open.len();
|
||||
if close.len() != n {
|
||||
return Err(JsError::new("open, close must be equal length"));
|
||||
}
|
||||
let mut out = vec![f64::NAN; n];
|
||||
for i in 0..n {
|
||||
let hi = open[i].max(close[i]);
|
||||
let lo = open[i].min(close[i]);
|
||||
let c = make_candle_ohlc(open[i], hi, lo, close[i])?;
|
||||
if let Some(v) = self.inner.update(c) {
|
||||
out[i] = v;
|
||||
}
|
||||
}
|
||||
Ok(Float64Array::from(out.as_slice()))
|
||||
}
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
/// Streaming update over one candle's open and close.
|
||||
pub fn update(&mut self, open: f64, close: f64) -> Result<Option<f64>, JsError> {
|
||||
let hi = open.max(close);
|
||||
let lo = open.min(close);
|
||||
let c = make_candle_ohlc(open, hi, lo, close)?;
|
||||
Ok(self.inner.update(c))
|
||||
}
|
||||
#[wasm_bindgen(js_name = isReady)]
|
||||
pub fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
#[wasm_bindgen(js_name = warmupPeriod)]
|
||||
pub fn warmup_period(&self) -> usize {
|
||||
self.inner.warmup_period()
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = GatorOscillator)]
|
||||
pub struct WasmGatorOscillator {
|
||||
inner: wc::GatorOscillator,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = GatorOscillator)]
|
||||
impl WasmGatorOscillator {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(
|
||||
jaw_period: usize,
|
||||
teeth_period: usize,
|
||||
lips_period: usize,
|
||||
) -> Result<WasmGatorOscillator, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::GatorOscillator::new(jaw_period, teeth_period, lips_period)
|
||||
.map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
/// Returns `[upper0, lower0, upper1, lower1, ...]`, length `2 * n`.
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
high: &[f64],
|
||||
low: &[f64],
|
||||
close: &[f64],
|
||||
) -> Result<Float64Array, JsError> {
|
||||
let n = high.len();
|
||||
if low.len() != n || close.len() != n {
|
||||
return Err(JsError::new("high, low, close must be equal length"));
|
||||
}
|
||||
let mut out = vec![f64::NAN; n * 2];
|
||||
for i in 0..n {
|
||||
let c = make_candle(high[i], low[i], close[i], 0.0)?;
|
||||
if let Some(o) = self.inner.update(c) {
|
||||
out[i * 2] = o.upper;
|
||||
out[i * 2 + 1] = o.lower;
|
||||
}
|
||||
}
|
||||
Ok(Float64Array::from(out.as_slice()))
|
||||
}
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
/// Streaming update. Returns `{ upper, lower }` once warm, else `null`.
|
||||
pub fn update(&mut self, high: f64, low: f64, close: f64) -> Result<JsValue, JsError> {
|
||||
let c = make_candle(high, low, close, 0.0)?;
|
||||
Ok(match self.inner.update(c) {
|
||||
Some(o) => {
|
||||
let obj = Object::new();
|
||||
Reflect::set(&obj, &"upper".into(), &o.upper.into()).ok();
|
||||
Reflect::set(&obj, &"lower".into(), &o.lower.into()).ok();
|
||||
obj.into()
|
||||
}
|
||||
None => JsValue::NULL,
|
||||
})
|
||||
}
|
||||
#[wasm_bindgen(js_name = isReady)]
|
||||
pub fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
#[wasm_bindgen(js_name = warmupPeriod)]
|
||||
pub fn warmup_period(&self) -> usize {
|
||||
self.inner.warmup_period()
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = KasePermissionStochastic)]
|
||||
pub struct WasmKasePermissionStochastic {
|
||||
inner: wc::KasePermissionStochastic,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = KasePermissionStochastic)]
|
||||
impl WasmKasePermissionStochastic {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(length: usize, smooth: usize) -> Result<WasmKasePermissionStochastic, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::KasePermissionStochastic::new(length, smooth).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
/// Returns `[fast0, slow0, fast1, slow1, ...]`, length `2 * n`.
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
high: &[f64],
|
||||
low: &[f64],
|
||||
close: &[f64],
|
||||
) -> Result<Float64Array, JsError> {
|
||||
let n = high.len();
|
||||
if low.len() != n || close.len() != n {
|
||||
return Err(JsError::new("high, low, close must be equal length"));
|
||||
}
|
||||
let mut out = vec![f64::NAN; n * 2];
|
||||
for i in 0..n {
|
||||
let c = make_candle(high[i], low[i], close[i], 0.0)?;
|
||||
if let Some(o) = self.inner.update(c) {
|
||||
out[i * 2] = o.fast;
|
||||
out[i * 2 + 1] = o.slow;
|
||||
}
|
||||
}
|
||||
Ok(Float64Array::from(out.as_slice()))
|
||||
}
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
/// Streaming update. Returns `{ fast, slow }` once warm, else `null`.
|
||||
pub fn update(&mut self, high: f64, low: f64, close: f64) -> Result<JsValue, JsError> {
|
||||
let c = make_candle(high, low, close, 0.0)?;
|
||||
Ok(match self.inner.update(c) {
|
||||
Some(o) => {
|
||||
let obj = Object::new();
|
||||
Reflect::set(&obj, &"fast".into(), &o.fast.into()).ok();
|
||||
Reflect::set(&obj, &"slow".into(), &o.slow.into()).ok();
|
||||
obj.into()
|
||||
}
|
||||
None => JsValue::NULL,
|
||||
})
|
||||
}
|
||||
#[wasm_bindgen(js_name = isReady)]
|
||||
pub fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
#[wasm_bindgen(js_name = warmupPeriod)]
|
||||
pub fn warmup_period(&self) -> usize {
|
||||
self.inner.warmup_period()
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = Stochastic)]
|
||||
pub struct WasmStoch {
|
||||
inner: wc::Stochastic,
|
||||
@@ -10431,6 +10652,7 @@ wasm_scalar_indicator!(WasmRsx, "RSX", wc::Rsx, period: usize);
|
||||
wasm_scalar_indicator!(WasmDynamicMomentumIndex, "DynamicMomentumIndex", wc::DynamicMomentumIndex, period: usize);
|
||||
wasm_scalar_indicator!(WasmRmi, "RMI", wc::Rmi, period: usize, momentum: usize);
|
||||
wasm_scalar_indicator!(WasmDerivativeOscillator, "DerivativeOscillator", wc::DerivativeOscillator, rsi_period: usize, smooth1: usize, smooth2: usize, signal_period: usize);
|
||||
wasm_scalar_indicator!(WasmTrendStrengthIndex, "TREND_STRENGTH_INDEX", wc::TrendStrengthIndex, period: usize);
|
||||
|
||||
// --- DrawdownDuration: u32 output, no constructor args ---
|
||||
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
//! Bill Williams' Gator Oscillator (derived from the Alligator).
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::indicators::alligator::Alligator;
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Gator Oscillator output: the two histogram bars drawn above and below the
|
||||
/// zero line.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct GatorOscillatorOutput {
|
||||
/// Upper histogram `|jaw - teeth|`, always `>= 0`.
|
||||
pub upper: f64,
|
||||
/// Lower histogram `-|teeth - lips|`, always `<= 0`.
|
||||
pub lower: f64,
|
||||
}
|
||||
|
||||
/// Bill Williams' Gator Oscillator: a convergence/divergence view of the
|
||||
/// [`Alligator`] lines. The upper bar is the absolute gap between Jaw and
|
||||
/// Teeth; the lower bar is the negated absolute gap between Teeth and Lips.
|
||||
///
|
||||
/// ```text
|
||||
/// upper = |jaw - teeth|
|
||||
/// lower = -|teeth - lips |
|
||||
/// ```
|
||||
///
|
||||
/// Widening bars mean the Alligator's mouth is opening (a trending market);
|
||||
/// shrinking bars mean it is closing (consolidation). Warmup matches the
|
||||
/// underlying Alligator — the first value appears once the slowest line (Jaw)
|
||||
/// has warmed up.
|
||||
///
|
||||
/// Reference: Bill Williams, *Trading Chaos*, 1995.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Candle, GatorOscillator, Indicator};
|
||||
///
|
||||
/// let mut indicator = GatorOscillator::classic();
|
||||
/// let mut last = None;
|
||||
/// for i in 0..40 {
|
||||
/// let base = 100.0 + f64::from(i);
|
||||
/// let candle =
|
||||
/// Candle::new(base, base + 1.0, base - 1.0, base, 1.0, i64::from(i)).unwrap();
|
||||
/// last = indicator.update(candle);
|
||||
/// }
|
||||
/// assert!(last.is_some());
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GatorOscillator {
|
||||
alligator: Alligator,
|
||||
}
|
||||
|
||||
impl GatorOscillator {
|
||||
/// Construct a Gator Oscillator from explicit Alligator periods
|
||||
/// `(jaw, teeth, lips)`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::PeriodZero`](crate::error::Error::PeriodZero) if any period is zero.
|
||||
pub fn new(jaw_period: usize, teeth_period: usize, lips_period: usize) -> Result<Self> {
|
||||
Ok(Self {
|
||||
alligator: Alligator::new(jaw_period, teeth_period, lips_period)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Bill Williams' classic parameters: `(jaw = 13, teeth = 8, lips = 5)`.
|
||||
pub fn classic() -> Self {
|
||||
Self {
|
||||
alligator: Alligator::classic(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Configured `(jaw_period, teeth_period, lips_period)`.
|
||||
pub const fn periods(&self) -> (usize, usize, usize) {
|
||||
self.alligator.periods()
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for GatorOscillator {
|
||||
type Input = Candle;
|
||||
type Output = GatorOscillatorOutput;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<GatorOscillatorOutput> {
|
||||
let lines = self.alligator.update(candle)?;
|
||||
Some(GatorOscillatorOutput {
|
||||
upper: (lines.jaw - lines.teeth).abs(),
|
||||
lower: -(lines.teeth - lines.lips).abs(),
|
||||
})
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.alligator.reset();
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.alligator.warmup_period()
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.alligator.is_ready()
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"GatorOscillator"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::error::Error;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
fn candle(high: f64, low: f64, ts: i64) -> Candle {
|
||||
let close = f64::midpoint(high, low);
|
||||
Candle::new(close, high, low, close, 1.0, ts).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_zero_period() {
|
||||
assert!(matches!(
|
||||
GatorOscillator::new(0, 8, 5),
|
||||
Err(Error::PeriodZero)
|
||||
));
|
||||
assert!(matches!(
|
||||
GatorOscillator::new(13, 0, 5),
|
||||
Err(Error::PeriodZero)
|
||||
));
|
||||
assert!(matches!(
|
||||
GatorOscillator::new(13, 8, 0),
|
||||
Err(Error::PeriodZero)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let g = GatorOscillator::classic();
|
||||
assert_eq!(g.periods(), (13, 8, 5));
|
||||
assert_eq!(g.warmup_period(), 13);
|
||||
assert_eq!(g.name(), "GatorOscillator");
|
||||
assert!(!g.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn constant_series_collapses_both_bars() {
|
||||
// All three Alligator lines equal the constant median -> zero spread.
|
||||
let mut g = GatorOscillator::classic();
|
||||
let candles: Vec<Candle> = (0..40).map(|i| candle(11.0, 9.0, i)).collect();
|
||||
let out = g.batch(&candles);
|
||||
let last = out.last().unwrap().unwrap();
|
||||
assert_relative_eq!(last.upper, 0.0, epsilon = 1e-12);
|
||||
assert_relative_eq!(last.lower, 0.0, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trending_series_opens_the_mouth() {
|
||||
// On a clean trend the lines separate -> upper > 0, lower < 0.
|
||||
let mut g = GatorOscillator::classic();
|
||||
let candles: Vec<Candle> = (0_i64..80)
|
||||
.map(|i| candle(10.0 + i as f64, 9.0 + i as f64, i))
|
||||
.collect();
|
||||
let last = g.batch(&candles).last().unwrap().unwrap();
|
||||
assert!(last.upper > 0.0, "upper {} should be positive", last.upper);
|
||||
assert!(last.lower < 0.0, "lower {} should be negative", last.lower);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn warmup_emits_first_value_at_longest_period() {
|
||||
let mut g = GatorOscillator::new(5, 3, 2).unwrap();
|
||||
let candles: Vec<Candle> = (0..6).map(|i| candle(11.0, 9.0, i)).collect();
|
||||
let out = g.batch(&candles);
|
||||
for v in out.iter().take(4) {
|
||||
assert!(v.is_none());
|
||||
}
|
||||
assert!(out[4].is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut g = GatorOscillator::classic();
|
||||
let candles: Vec<Candle> = (0..40).map(|i| candle(11.0, 9.0, i)).collect();
|
||||
g.batch(&candles);
|
||||
assert!(g.is_ready());
|
||||
g.reset();
|
||||
assert!(!g.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles: Vec<Candle> = (0..80_i64)
|
||||
.map(|i| {
|
||||
let base = 100.0 + (i as f64 * 0.2).sin() * 5.0;
|
||||
candle(base + 1.0, base - 1.0, i)
|
||||
})
|
||||
.collect();
|
||||
let mut a = GatorOscillator::classic();
|
||||
let mut b = GatorOscillator::classic();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|c| b.update(*c)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
//! Kase Permission Stochastic — a double-smoothed stochastic used as a
|
||||
//! trade-permission filter.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::indicators::ema::Ema;
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Kase Permission Stochastic output: a fast and a slow line.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct KasePermissionStochasticOutput {
|
||||
/// Fast line: EMA of the raw `%K` over the smoothing period.
|
||||
pub fast: f64,
|
||||
/// Slow line: EMA of the fast line over the smoothing period.
|
||||
pub slow: f64,
|
||||
}
|
||||
|
||||
/// Cynthia Kase's Permission Stochastic: a stochastic oscillator smoothed twice,
|
||||
/// whose fast/slow relationship grants or denies "permission" to trade in the
|
||||
/// direction of a higher-timeframe signal.
|
||||
///
|
||||
/// ```text
|
||||
/// raw%K = 100 * (close - LL) / (HH - LL) over `length` (50 when HH == LL)
|
||||
/// fast = EMA(raw%K, smooth)
|
||||
/// slow = EMA(fast, smooth)
|
||||
/// ```
|
||||
///
|
||||
/// The raw stochastic is the usual `%K`, then an EMA produces the *fast* line
|
||||
/// and a second EMA of that produces the *slow* line. Kase uses the pair as a
|
||||
/// gate: a fast line above the slow line (and rising) gives permission for
|
||||
/// longs, the reverse for shorts. When the lookback window is perfectly flat
|
||||
/// (`HH == LL`), the raw stochastic is undefined and defaults to the neutral
|
||||
/// `50`.
|
||||
///
|
||||
/// Reference: Cynthia Kase, *Trading with the Odds*, 1996.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Candle, Indicator, KasePermissionStochastic};
|
||||
///
|
||||
/// let mut indicator = KasePermissionStochastic::new(9, 3).unwrap();
|
||||
/// let mut last = None;
|
||||
/// for i in 0..40 {
|
||||
/// let base = 100.0 + f64::from(i);
|
||||
/// let candle =
|
||||
/// Candle::new(base, base + 2.0, base - 2.0, base + 1.0, 1.0, i64::from(i)).unwrap();
|
||||
/// last = indicator.update(candle);
|
||||
/// }
|
||||
/// assert!(last.is_some());
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct KasePermissionStochastic {
|
||||
length: usize,
|
||||
smooth: usize,
|
||||
window: VecDeque<(f64, f64)>,
|
||||
fast_ema: Ema,
|
||||
slow_ema: Ema,
|
||||
}
|
||||
|
||||
impl KasePermissionStochastic {
|
||||
/// Construct with the stochastic `length` and the EMA `smooth` period
|
||||
/// applied twice.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::PeriodZero`] if `length == 0` or `smooth == 0`.
|
||||
pub fn new(length: usize, smooth: usize) -> Result<Self> {
|
||||
if length == 0 {
|
||||
return Err(Error::PeriodZero);
|
||||
}
|
||||
Ok(Self {
|
||||
length,
|
||||
smooth,
|
||||
window: VecDeque::with_capacity(length),
|
||||
fast_ema: Ema::new(smooth)?,
|
||||
slow_ema: Ema::new(smooth)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Cynthia Kase's classic parameters: `length = 9`, `smooth = 3`.
|
||||
pub fn classic() -> Self {
|
||||
Self::new(9, 3).expect("classic Kase Permission Stochastic parameters are valid")
|
||||
}
|
||||
|
||||
/// Configured `(length, smooth)`.
|
||||
pub const fn periods(&self) -> (usize, usize) {
|
||||
(self.length, self.smooth)
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for KasePermissionStochastic {
|
||||
type Input = Candle;
|
||||
type Output = KasePermissionStochasticOutput;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<KasePermissionStochasticOutput> {
|
||||
self.window.push_back((candle.high, candle.low));
|
||||
if self.window.len() > self.length {
|
||||
self.window.pop_front();
|
||||
}
|
||||
if self.window.len() < self.length {
|
||||
return None;
|
||||
}
|
||||
|
||||
let highest = self.window.iter().map(|w| w.0).fold(f64::MIN, f64::max);
|
||||
let lowest = self.window.iter().map(|w| w.1).fold(f64::MAX, f64::min);
|
||||
let raw_k = if highest > lowest {
|
||||
100.0 * (candle.close - lowest) / (highest - lowest)
|
||||
} else {
|
||||
50.0
|
||||
};
|
||||
|
||||
let fast = self.fast_ema.update(raw_k)?;
|
||||
let slow = self.slow_ema.update(fast)?;
|
||||
Some(KasePermissionStochasticOutput { fast, slow })
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.window.clear();
|
||||
self.fast_ema.reset();
|
||||
self.slow_ema.reset();
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
// raw%K ready after `length` bars; each EMA seeds over `smooth` values.
|
||||
self.length + 2 * self.smooth - 2
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.slow_ema.is_ready()
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"KasePermissionStochastic"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
fn candle(high: f64, low: f64, close: f64, ts: i64) -> Candle {
|
||||
Candle::new(f64::midpoint(high, low), high, low, close, 1.0, ts).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_zero_period() {
|
||||
assert!(matches!(
|
||||
KasePermissionStochastic::new(0, 3),
|
||||
Err(Error::PeriodZero)
|
||||
));
|
||||
assert!(matches!(
|
||||
KasePermissionStochastic::new(9, 0),
|
||||
Err(Error::PeriodZero)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let k = KasePermissionStochastic::classic();
|
||||
assert_eq!(k.periods(), (9, 3));
|
||||
// 9 + 2*3 - 2 = 13.
|
||||
assert_eq!(k.warmup_period(), 13);
|
||||
assert_eq!(k.name(), "KasePermissionStochastic");
|
||||
assert!(!k.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn warmup_emits_at_expected_bar() {
|
||||
let mut k = KasePermissionStochastic::new(3, 2).unwrap();
|
||||
// warmup = 3 + 2*2 - 2 = 5 -> first value at input 5 (index 4).
|
||||
let candles: Vec<Candle> = (0..8).map(|i| candle(11.0, 9.0, 10.5, i)).collect();
|
||||
let out = k.batch(&candles);
|
||||
assert!(out[3].is_none());
|
||||
assert!(out[4].is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn top_of_range_is_high() {
|
||||
// Close pinned at the top of a rising range -> raw%K near 100, both
|
||||
// smoothed lines high.
|
||||
let mut k = KasePermissionStochastic::new(5, 3).unwrap();
|
||||
let candles: Vec<Candle> = (0_i64..40)
|
||||
.map(|i| {
|
||||
let base = 100.0 + i as f64;
|
||||
candle(base + 2.0, base - 2.0, base + 2.0, i)
|
||||
})
|
||||
.collect();
|
||||
let last = k.batch(&candles).last().unwrap().unwrap();
|
||||
assert!(last.fast > 80.0, "fast {} should be high", last.fast);
|
||||
assert!(last.slow > 80.0, "slow {} should be high", last.slow);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flat_window_defaults_to_neutral() {
|
||||
// Constant high/low/close -> HH == LL -> raw%K defaults to 50, so both
|
||||
// EMAs converge to 50.
|
||||
let mut k = KasePermissionStochastic::new(4, 2).unwrap();
|
||||
let candles: Vec<Candle> = (0..20).map(|i| candle(10.0, 10.0, 10.0, i)).collect();
|
||||
let last = k.batch(&candles).last().unwrap().unwrap();
|
||||
assert_relative_eq!(last.fast, 50.0, epsilon = 1e-9);
|
||||
assert_relative_eq!(last.slow, 50.0, epsilon = 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut k = KasePermissionStochastic::classic();
|
||||
let candles: Vec<Candle> = (0..40).map(|i| candle(11.0, 9.0, 10.5, i)).collect();
|
||||
k.batch(&candles);
|
||||
assert!(k.is_ready());
|
||||
k.reset();
|
||||
assert!(!k.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles: Vec<Candle> = (0..80_i64)
|
||||
.map(|i| {
|
||||
let base = 100.0 + (i as f64 * 0.2).sin() * 5.0;
|
||||
candle(base + 2.0, base - 2.0, base + (i as f64 * 0.3).cos(), i)
|
||||
})
|
||||
.collect();
|
||||
let mut a = KasePermissionStochastic::classic();
|
||||
let mut b = KasePermissionStochastic::classic();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|c| b.update(*c)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -145,6 +145,7 @@ mod gain_loss_ratio;
|
||||
mod gap_side_by_side_white;
|
||||
mod garman_klass;
|
||||
mod gartley;
|
||||
mod gator_oscillator;
|
||||
mod generalized_dema;
|
||||
mod geometric_ma;
|
||||
mod golden_pocket;
|
||||
@@ -187,6 +188,7 @@ mod jump_indicator;
|
||||
mod kagi_bars;
|
||||
mod kalman_hedge_ratio;
|
||||
mod kama;
|
||||
mod kase_permission_stochastic;
|
||||
mod kelly_criterion;
|
||||
mod keltner;
|
||||
mod kicking;
|
||||
@@ -266,11 +268,13 @@ mod plus_di;
|
||||
mod plus_dm;
|
||||
mod pmo;
|
||||
mod point_and_figure_bars;
|
||||
mod polarized_fractal_efficiency;
|
||||
mod ppo;
|
||||
mod profit_factor;
|
||||
mod psar;
|
||||
mod pvi;
|
||||
mod qqe;
|
||||
mod qstick;
|
||||
mod quoted_spread;
|
||||
mod r_squared;
|
||||
mod realized_spread;
|
||||
@@ -368,6 +372,7 @@ mod time_of_day_return_profile;
|
||||
mod tpo_profile;
|
||||
mod trade_imbalance;
|
||||
mod trend_label;
|
||||
mod trend_strength_index;
|
||||
mod treynor_ratio;
|
||||
mod triangle;
|
||||
mod trima;
|
||||
@@ -379,6 +384,7 @@ mod tsf;
|
||||
mod tsi;
|
||||
mod tsv;
|
||||
mod ttm_squeeze;
|
||||
mod ttm_trend;
|
||||
mod turn_of_month;
|
||||
mod tweezer;
|
||||
mod two_crows;
|
||||
@@ -406,6 +412,7 @@ mod vwap;
|
||||
mod vwap_stddev_bands;
|
||||
mod vwma;
|
||||
mod vzo;
|
||||
mod wave_pm;
|
||||
mod wave_trend;
|
||||
mod wedge;
|
||||
mod weighted_close;
|
||||
@@ -558,6 +565,7 @@ pub use gain_loss_ratio::GainLossRatio;
|
||||
pub use gap_side_by_side_white::GapSideBySideWhite;
|
||||
pub use garman_klass::GarmanKlassVolatility;
|
||||
pub use gartley::Gartley;
|
||||
pub use gator_oscillator::GatorOscillator;
|
||||
pub use generalized_dema::GeneralizedDema;
|
||||
pub use geometric_ma::GeometricMa;
|
||||
pub use golden_pocket::{GoldenPocket, GoldenPocketOutput};
|
||||
@@ -600,6 +608,7 @@ pub use jump_indicator::JumpIndicator;
|
||||
pub use kagi_bars::{KagiBar, KagiBars};
|
||||
pub use kalman_hedge_ratio::{KalmanHedgeRatio, KalmanHedgeRatioOutput};
|
||||
pub use kama::Kama;
|
||||
pub use kase_permission_stochastic::KasePermissionStochastic;
|
||||
pub use kelly_criterion::KellyCriterion;
|
||||
pub use keltner::{Keltner, KeltnerOutput};
|
||||
pub use kicking::Kicking;
|
||||
@@ -679,11 +688,13 @@ pub use plus_di::PlusDi;
|
||||
pub use plus_dm::PlusDm;
|
||||
pub use pmo::Pmo;
|
||||
pub use point_and_figure_bars::{PnfColumn, PointAndFigureBars};
|
||||
pub use polarized_fractal_efficiency::PolarizedFractalEfficiency;
|
||||
pub use ppo::Ppo;
|
||||
pub use profit_factor::ProfitFactor;
|
||||
pub use psar::Psar;
|
||||
pub use pvi::Pvi;
|
||||
pub use qqe::{Qqe, QqeOutput};
|
||||
pub use qstick::Qstick;
|
||||
pub use quoted_spread::QuotedSpread;
|
||||
pub use r_squared::RSquared;
|
||||
pub use realized_spread::RealizedSpread;
|
||||
@@ -781,6 +792,7 @@ pub use time_of_day_return_profile::{TimeOfDayReturnProfile, TimeOfDayReturnProf
|
||||
pub use tpo_profile::{TpoProfile, TpoProfileOutput};
|
||||
pub use trade_imbalance::TradeImbalance;
|
||||
pub use trend_label::TrendLabel;
|
||||
pub use trend_strength_index::TrendStrengthIndex;
|
||||
pub use treynor_ratio::TreynorRatio;
|
||||
pub use triangle::Triangle;
|
||||
pub use trima::Trima;
|
||||
@@ -792,6 +804,7 @@ pub use tsf::Tsf;
|
||||
pub use tsi::Tsi;
|
||||
pub use tsv::Tsv;
|
||||
pub use ttm_squeeze::{TtmSqueeze, TtmSqueezeOutput};
|
||||
pub use ttm_trend::TtmTrend;
|
||||
pub use turn_of_month::TurnOfMonth;
|
||||
pub use tweezer::Tweezer;
|
||||
pub use two_crows::TwoCrows;
|
||||
@@ -819,6 +832,7 @@ pub use vwap::{RollingVwap, Vwap};
|
||||
pub use vwap_stddev_bands::{VwapStdDevBands, VwapStdDevBandsOutput};
|
||||
pub use vwma::Vwma;
|
||||
pub use vzo::Vzo;
|
||||
pub use wave_pm::WavePm;
|
||||
pub use wave_trend::{WaveTrend, WaveTrendOutput};
|
||||
pub use wedge::Wedge;
|
||||
pub use weighted_close::WeightedClose;
|
||||
@@ -936,6 +950,13 @@ pub const FAMILIES: &[(&str, &[&str])] = &[
|
||||
"MinusDi",
|
||||
"Dx",
|
||||
"TrendLabel",
|
||||
"TtmTrend",
|
||||
"TrendStrengthIndex",
|
||||
"Qstick",
|
||||
"PolarizedFractalEfficiency",
|
||||
"WavePm",
|
||||
"GatorOscillator",
|
||||
"KasePermissionStochastic",
|
||||
],
|
||||
),
|
||||
(
|
||||
@@ -1393,6 +1414,6 @@ mod family_tests {
|
||||
// the actual indicator count is the early-warning signal that an
|
||||
// indicator was added without being assigned a family.
|
||||
let total: usize = FAMILIES.iter().map(|(_, ns)| ns.len()).sum();
|
||||
assert_eq!(total, 413, "FAMILIES total drifted from indicator count");
|
||||
assert_eq!(total, 420, "FAMILIES total drifted from indicator count");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
//! Polarized Fractal Efficiency (PFE).
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::indicators::ema::Ema;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Polarized Fractal Efficiency: how efficiently price travelled over the last
|
||||
/// `period` bars, signed by direction and smoothed by an EMA.
|
||||
///
|
||||
/// ```text
|
||||
/// straight = sqrt((C_t - C_{t-n})^2 + n^2) (direct distance over n bars)
|
||||
/// path = Σ_{i=1..n} sqrt((C_{t-i+1} - C_{t-i})^2 + 1) (sum of single-bar steps)
|
||||
/// raw = 100 * sign(C_t - C_{t-n}) * straight / path
|
||||
/// PFE = EMA(raw, smoothing)
|
||||
/// ```
|
||||
///
|
||||
/// The ratio `straight / path` is the fractal efficiency: it is `1` when price
|
||||
/// moved in a perfectly straight line and falls toward `0` as the path becomes
|
||||
/// jagged. Polarizing it by the sign of the net move pushes the reading to
|
||||
/// `+100` for an efficient up-move and `-100` for an efficient down-move, with
|
||||
/// choppy markets oscillating near zero. Because each single-bar step and the
|
||||
/// `n`-bar diagonal both carry the bar count on the x-axis (`+1` and `+n^2`),
|
||||
/// the path length is always `>= n`, so the denominator can never be zero.
|
||||
///
|
||||
/// Reference: Hans Hannula, *Stocks & Commodities*, 1994.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Indicator, PolarizedFractalEfficiency};
|
||||
///
|
||||
/// let mut indicator = PolarizedFractalEfficiency::new(10, 5).unwrap();
|
||||
/// let mut last = None;
|
||||
/// for i in 0..40 {
|
||||
/// last = indicator.update(100.0 + f64::from(i));
|
||||
/// }
|
||||
/// assert!(last.is_some());
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PolarizedFractalEfficiency {
|
||||
period: usize,
|
||||
smoothing: usize,
|
||||
closes: VecDeque<f64>,
|
||||
prev_close: Option<f64>,
|
||||
segments: VecDeque<f64>,
|
||||
segment_sum: f64,
|
||||
ema: Ema,
|
||||
}
|
||||
|
||||
impl PolarizedFractalEfficiency {
|
||||
/// Construct a PFE with the fractal lookback `period` and the EMA
|
||||
/// `smoothing` period.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::PeriodZero`] if `period == 0` or `smoothing == 0`.
|
||||
pub fn new(period: usize, smoothing: usize) -> Result<Self> {
|
||||
if period == 0 {
|
||||
return Err(Error::PeriodZero);
|
||||
}
|
||||
Ok(Self {
|
||||
period,
|
||||
smoothing,
|
||||
closes: VecDeque::with_capacity(period + 1),
|
||||
prev_close: None,
|
||||
segments: VecDeque::with_capacity(period),
|
||||
segment_sum: 0.0,
|
||||
ema: Ema::new(smoothing)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured `(period, smoothing)`.
|
||||
pub const fn periods(&self) -> (usize, usize) {
|
||||
(self.period, self.smoothing)
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for PolarizedFractalEfficiency {
|
||||
type Input = f64;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, close: f64) -> Option<f64> {
|
||||
if let Some(prev) = self.prev_close {
|
||||
let diff = close - prev;
|
||||
let segment = diff.mul_add(diff, 1.0).sqrt();
|
||||
self.segment_sum += segment;
|
||||
self.segments.push_back(segment);
|
||||
if self.segments.len() > self.period {
|
||||
self.segment_sum -= self.segments.pop_front().unwrap_or(0.0);
|
||||
}
|
||||
}
|
||||
self.prev_close = Some(close);
|
||||
|
||||
self.closes.push_back(close);
|
||||
if self.closes.len() > self.period + 1 {
|
||||
self.closes.pop_front();
|
||||
}
|
||||
if self.closes.len() <= self.period {
|
||||
return None;
|
||||
}
|
||||
|
||||
let oldest = *self.closes.front().unwrap_or(&close);
|
||||
let net = close - oldest;
|
||||
let direction = if net > 0.0 {
|
||||
1.0
|
||||
} else if net < 0.0 {
|
||||
-1.0
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let span = self.period as f64;
|
||||
let straight = net.mul_add(net, span * span).sqrt();
|
||||
let raw = 100.0 * direction * straight / self.segment_sum;
|
||||
self.ema.update(raw)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.closes.clear();
|
||||
self.prev_close = None;
|
||||
self.segments.clear();
|
||||
self.segment_sum = 0.0;
|
||||
self.ema.reset();
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.period + self.smoothing
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.ema.is_ready()
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"PolarizedFractalEfficiency"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
#[test]
|
||||
fn rejects_zero_period() {
|
||||
assert!(matches!(
|
||||
PolarizedFractalEfficiency::new(0, 5),
|
||||
Err(Error::PeriodZero)
|
||||
));
|
||||
assert!(matches!(
|
||||
PolarizedFractalEfficiency::new(10, 0),
|
||||
Err(Error::PeriodZero)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let pfe = PolarizedFractalEfficiency::new(10, 5).unwrap();
|
||||
assert_eq!(pfe.periods(), (10, 5));
|
||||
assert_eq!(pfe.warmup_period(), 15);
|
||||
assert_eq!(pfe.name(), "PolarizedFractalEfficiency");
|
||||
assert!(!pfe.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn warmup_emits_after_period_plus_smoothing() {
|
||||
let mut pfe = PolarizedFractalEfficiency::new(4, 2).unwrap();
|
||||
// raw needs period+1 = 5 closes; EMA(2) needs 2 raws -> first value at
|
||||
// input 6 (index 5).
|
||||
let inputs: Vec<f64> = (0..10).map(f64::from).collect();
|
||||
let out = pfe.batch(&inputs);
|
||||
assert!(out[4].is_none());
|
||||
assert!(out[5].is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn perfect_uptrend_is_strongly_positive() {
|
||||
// A straight ramp: every step is +1, the diagonal is maximally
|
||||
// efficient, so PFE saturates near +100.
|
||||
let mut pfe = PolarizedFractalEfficiency::new(5, 3).unwrap();
|
||||
let inputs: Vec<f64> = (0..30).map(f64::from).collect();
|
||||
let last = pfe.batch(&inputs).last().unwrap().unwrap();
|
||||
assert!(last > 99.0, "pfe {last} should be near +100");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn perfect_downtrend_is_strongly_negative() {
|
||||
let mut pfe = PolarizedFractalEfficiency::new(5, 3).unwrap();
|
||||
let inputs: Vec<f64> = (0..30).map(|i| -f64::from(i)).collect();
|
||||
let last = pfe.batch(&inputs).last().unwrap().unwrap();
|
||||
assert!(last < -99.0, "pfe {last} should be near -100");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flat_market_returns_zero() {
|
||||
// No net move over the window -> direction 0 -> raw 0 -> PFE 0.
|
||||
let mut pfe = PolarizedFractalEfficiency::new(5, 3).unwrap();
|
||||
let inputs = [10.0; 20];
|
||||
let last = pfe.batch(&inputs).last().unwrap().unwrap();
|
||||
assert_relative_eq!(last, 0.0, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn choppy_market_is_inefficient() {
|
||||
// A sawtooth whip: the net move is tiny relative to the jagged path, so
|
||||
// efficiency stays well below the +-100 saturation of a clean trend.
|
||||
let mut pfe = PolarizedFractalEfficiency::new(5, 3).unwrap();
|
||||
let inputs: Vec<f64> = (0..40)
|
||||
.map(|i| if i % 2 == 0 { 100.0 } else { 102.0 })
|
||||
.collect();
|
||||
let last = pfe.batch(&inputs).last().unwrap().unwrap();
|
||||
assert!(
|
||||
last.abs() < 60.0,
|
||||
"choppy pfe {last} should be far from +-100"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut pfe = PolarizedFractalEfficiency::new(5, 3).unwrap();
|
||||
let inputs: Vec<f64> = (0..30).map(f64::from).collect();
|
||||
pfe.batch(&inputs);
|
||||
assert!(pfe.is_ready());
|
||||
pfe.reset();
|
||||
assert!(!pfe.is_ready());
|
||||
assert_eq!(pfe.periods(), (5, 3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let inputs: Vec<f64> = (0..80)
|
||||
.map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 5.0)
|
||||
.collect();
|
||||
let mut a = PolarizedFractalEfficiency::new(10, 5).unwrap();
|
||||
let mut b = PolarizedFractalEfficiency::new(10, 5).unwrap();
|
||||
assert_eq!(
|
||||
a.batch(&inputs),
|
||||
inputs.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
//! Qstick — Tushar Chande's measure of buying vs. selling pressure.
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::indicators::sma::Sma;
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Qstick: the simple moving average of the body `close - open` over `period`
|
||||
/// bars.
|
||||
///
|
||||
/// Positive values indicate a run of bars that closed above their open (net
|
||||
/// buying pressure); negative values indicate net selling pressure. A zero
|
||||
/// crossing is read as a shift in short-term sentiment.
|
||||
///
|
||||
/// ```text
|
||||
/// Qstick = SMA(close - open, period)
|
||||
/// ```
|
||||
///
|
||||
/// Reference: Tushar Chande, *The New Technical Trader*, 1994.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Candle, Indicator, Qstick};
|
||||
///
|
||||
/// let mut indicator = Qstick::new(5).unwrap();
|
||||
/// let mut last = None;
|
||||
/// for i in 0..20 {
|
||||
/// let base = 100.0 + f64::from(i);
|
||||
/// let candle =
|
||||
/// Candle::new(base, base + 2.0, base - 1.0, base + 1.0, 1.0, i64::from(i)).unwrap();
|
||||
/// last = indicator.update(candle);
|
||||
/// }
|
||||
/// assert!(last.is_some());
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Qstick {
|
||||
period: usize,
|
||||
sma: Sma,
|
||||
}
|
||||
|
||||
impl Qstick {
|
||||
/// Construct a Qstick with the given averaging period.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::PeriodZero`](crate::error::Error::PeriodZero) if `period == 0`.
|
||||
pub fn new(period: usize) -> Result<Self> {
|
||||
Ok(Self {
|
||||
period,
|
||||
sma: Sma::new(period)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured averaging period.
|
||||
pub const fn period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for Qstick {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
self.sma.update(candle.close - candle.open)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.sma.reset();
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.sma.is_ready()
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"Qstick"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::error::Error;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
fn candle(open: f64, close: f64, ts: i64) -> Candle {
|
||||
let high = open.max(close) + 1.0;
|
||||
let low = open.min(close) - 1.0;
|
||||
Candle::new(open, high, low, close, 1.0, ts).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_zero_period() {
|
||||
assert!(matches!(Qstick::new(0), Err(Error::PeriodZero)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let q = Qstick::new(5).unwrap();
|
||||
assert_eq!(q.period(), 5);
|
||||
assert_eq!(q.warmup_period(), 5);
|
||||
assert_eq!(q.name(), "Qstick");
|
||||
assert!(!q.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn warmup_emits_first_value_at_period() {
|
||||
let mut q = Qstick::new(3).unwrap();
|
||||
let candles: Vec<Candle> = (0..3).map(|i| candle(10.0, 11.0, i)).collect();
|
||||
let out = q.batch(&candles);
|
||||
assert!(out[0].is_none());
|
||||
assert!(out[1].is_none());
|
||||
assert!(out[2].is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn constant_bodies_yield_the_body() {
|
||||
// Every bar closes 1.5 above its open -> Qstick converges to 1.5.
|
||||
let mut q = Qstick::new(4).unwrap();
|
||||
let candles: Vec<Candle> = (0..10).map(|i| candle(10.0, 11.5, i)).collect();
|
||||
let out = q.batch(&candles);
|
||||
assert_relative_eq!(out.last().unwrap().unwrap(), 1.5, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selling_pressure_is_negative() {
|
||||
let mut q = Qstick::new(3).unwrap();
|
||||
let candles: Vec<Candle> = (0..6).map(|i| candle(11.0, 10.0, i)).collect();
|
||||
let last = q.batch(&candles).last().unwrap().unwrap();
|
||||
assert!(last < 0.0, "qstick {last} should be negative");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut q = Qstick::new(3).unwrap();
|
||||
let candles: Vec<Candle> = (0..6).map(|i| candle(10.0, 11.0, i)).collect();
|
||||
q.batch(&candles);
|
||||
assert!(q.is_ready());
|
||||
q.reset();
|
||||
assert!(!q.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles: Vec<Candle> = (0..40_i64)
|
||||
.map(|i| {
|
||||
candle(
|
||||
100.0 + (i as f64 * 0.3).sin(),
|
||||
100.0 + (i as f64 * 0.4).cos(),
|
||||
i,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let mut a = Qstick::new(7).unwrap();
|
||||
let mut b = Qstick::new(7).unwrap();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|c| b.update(*c)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
//! Trend Strength Index — the signed coefficient of determination of a linear
|
||||
//! regression of price against time.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Trend Strength Index: fits an ordinary-least-squares line to the last
|
||||
/// `period` prices against their bar index and reports the coefficient of
|
||||
/// determination `r^2`, signed by the slope of the fit.
|
||||
///
|
||||
/// ```text
|
||||
/// regress y = close on x = 0..period-1
|
||||
/// r^2 = (n·Σxy − Σx·Σy)^2 / [ (n·Σx² − (Σx)²)(n·Σy² − (Σy)²) ]
|
||||
/// TSI = sign(slope) · r^2 (slope sign = sign of n·Σxy − Σx·Σy)
|
||||
/// ```
|
||||
///
|
||||
/// `r^2` in `[0, 1]` measures how well a straight line explains the price over
|
||||
/// the window — how *trendy* the segment is, regardless of direction. Carrying
|
||||
/// the slope sign turns it into a directional reading in `[-1, 1]`: values near
|
||||
/// `+1` are a strong, clean uptrend; near `-1` a strong downtrend; near `0` a
|
||||
/// flat or noisy market with no linear structure. A window of constant prices
|
||||
/// (zero variance in `y`) has no defined trend and returns `0`.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Indicator, TrendStrengthIndex};
|
||||
///
|
||||
/// let mut indicator = TrendStrengthIndex::new(20).unwrap();
|
||||
/// let mut last = None;
|
||||
/// for i in 0..40 {
|
||||
/// last = indicator.update(100.0 + f64::from(i));
|
||||
/// }
|
||||
/// // A clean ramp is a perfect uptrend -> r^2 = 1.
|
||||
/// assert!((last.unwrap() - 1.0).abs() < 1e-9);
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TrendStrengthIndex {
|
||||
period: usize,
|
||||
buf: VecDeque<f64>,
|
||||
}
|
||||
|
||||
impl TrendStrengthIndex {
|
||||
/// Construct a Trend Strength Index over the given window.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::PeriodZero`] if `period == 0`, or [`Error::InvalidPeriod`]
|
||||
/// if `period == 1` (a regression needs at least two points).
|
||||
pub fn new(period: usize) -> Result<Self> {
|
||||
if period == 0 {
|
||||
return Err(Error::PeriodZero);
|
||||
}
|
||||
if period == 1 {
|
||||
return Err(Error::InvalidPeriod {
|
||||
message: "period must be >= 2 for a regression",
|
||||
});
|
||||
}
|
||||
Ok(Self {
|
||||
period,
|
||||
buf: VecDeque::with_capacity(period),
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured window length.
|
||||
pub const fn period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for TrendStrengthIndex {
|
||||
type Input = f64;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, price: f64) -> Option<f64> {
|
||||
self.buf.push_back(price);
|
||||
if self.buf.len() > self.period {
|
||||
self.buf.pop_front();
|
||||
}
|
||||
if self.buf.len() < self.period {
|
||||
return None;
|
||||
}
|
||||
|
||||
let count = self.period as f64;
|
||||
let mut sum_x = 0.0;
|
||||
let mut sum_xx = 0.0;
|
||||
let mut sum_y = 0.0;
|
||||
let mut sum_yy = 0.0;
|
||||
let mut sum_xy = 0.0;
|
||||
for (idx, &price) in self.buf.iter().enumerate() {
|
||||
let x = idx as f64;
|
||||
sum_x += x;
|
||||
sum_xx += x * x;
|
||||
sum_y += price;
|
||||
sum_yy += price * price;
|
||||
sum_xy += x * price;
|
||||
}
|
||||
|
||||
let cov = count.mul_add(sum_xy, -(sum_x * sum_y));
|
||||
let var_x = count.mul_add(sum_xx, -(sum_x * sum_x));
|
||||
let var_y = count.mul_add(sum_yy, -(sum_y * sum_y));
|
||||
if var_y <= 0.0 {
|
||||
return Some(0.0);
|
||||
}
|
||||
let r2 = (cov * cov) / (var_x * var_y);
|
||||
Some(if cov >= 0.0 { r2 } else { -r2 })
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.buf.clear();
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.buf.len() >= self.period
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"TrendStrengthIndex"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
#[test]
|
||||
fn rejects_invalid_period() {
|
||||
assert!(matches!(TrendStrengthIndex::new(0), Err(Error::PeriodZero)));
|
||||
assert!(matches!(
|
||||
TrendStrengthIndex::new(1),
|
||||
Err(Error::InvalidPeriod { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let tsi = TrendStrengthIndex::new(20).unwrap();
|
||||
assert_eq!(tsi.period(), 20);
|
||||
assert_eq!(tsi.warmup_period(), 20);
|
||||
assert_eq!(tsi.name(), "TrendStrengthIndex");
|
||||
assert!(!tsi.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn warmup_emits_at_period() {
|
||||
let mut tsi = TrendStrengthIndex::new(4).unwrap();
|
||||
let inputs: Vec<f64> = (0..6).map(f64::from).collect();
|
||||
let out = tsi.batch(&inputs);
|
||||
assert!(out[2].is_none());
|
||||
assert!(out[3].is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn perfect_uptrend_is_plus_one() {
|
||||
let mut tsi = TrendStrengthIndex::new(10).unwrap();
|
||||
let inputs: Vec<f64> = (0..10).map(f64::from).collect();
|
||||
let last = tsi.batch(&inputs).last().unwrap().unwrap();
|
||||
assert_relative_eq!(last, 1.0, epsilon = 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn perfect_downtrend_is_minus_one() {
|
||||
let mut tsi = TrendStrengthIndex::new(10).unwrap();
|
||||
let inputs: Vec<f64> = (0..10).map(|i| 100.0 - f64::from(i)).collect();
|
||||
let last = tsi.batch(&inputs).last().unwrap().unwrap();
|
||||
assert_relative_eq!(last, -1.0, epsilon = 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flat_market_returns_zero() {
|
||||
let mut tsi = TrendStrengthIndex::new(8).unwrap();
|
||||
let inputs = [42.0; 12];
|
||||
let last = tsi.batch(&inputs).last().unwrap().unwrap();
|
||||
assert_relative_eq!(last, 0.0, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn noisy_trend_is_between() {
|
||||
// An upward drift with noise: positive but not a perfect fit.
|
||||
let mut tsi = TrendStrengthIndex::new(12).unwrap();
|
||||
let inputs: Vec<f64> = (0..12)
|
||||
.map(|i| f64::from(i) + if i % 2 == 0 { 0.0 } else { 3.0 })
|
||||
.collect();
|
||||
let last = tsi.batch(&inputs).last().unwrap().unwrap();
|
||||
assert!(last > 0.0 && last < 1.0, "tsi {last} should be in (0, 1)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut tsi = TrendStrengthIndex::new(10).unwrap();
|
||||
let inputs: Vec<f64> = (0..10).map(f64::from).collect();
|
||||
tsi.batch(&inputs);
|
||||
assert!(tsi.is_ready());
|
||||
tsi.reset();
|
||||
assert!(!tsi.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let inputs: Vec<f64> = (0..80)
|
||||
.map(|i| 100.0 + (f64::from(i) * 0.2).sin() * 5.0)
|
||||
.collect();
|
||||
let mut a = TrendStrengthIndex::new(15).unwrap();
|
||||
let mut b = TrendStrengthIndex::new(15).unwrap();
|
||||
assert_eq!(
|
||||
a.batch(&inputs),
|
||||
inputs.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
//! TTM Trend — John Carter's bar-coloring trend filter.
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::indicators::sma::Sma;
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// TTM Trend: compares the current close to the simple moving average of the
|
||||
/// recent median prices `(high + low) / 2`. A close above that reference colors
|
||||
/// the bar as an uptrend (`+1.0`); a close at or below it as a downtrend
|
||||
/// (`-1.0`).
|
||||
///
|
||||
/// ```text
|
||||
/// reference = SMA((high + low) / 2, period)
|
||||
/// TTM Trend = +1 if close > reference
|
||||
/// -1 otherwise
|
||||
/// ```
|
||||
///
|
||||
/// The classic TTM Trend uses the trailing six bars. The signal is a regime
|
||||
/// label rather than a level: it stays `None` during warmup and then emits
|
||||
/// `±1.0` on every bar.
|
||||
///
|
||||
/// Reference: John Carter, *Mastering the Trade*, 2005.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Candle, Indicator, TtmTrend};
|
||||
///
|
||||
/// let mut indicator = TtmTrend::new(6).unwrap();
|
||||
/// let mut last = None;
|
||||
/// for i in 0..20 {
|
||||
/// let base = 100.0 + f64::from(i);
|
||||
/// let candle =
|
||||
/// Candle::new(base, base + 1.0, base - 1.0, base + 0.5, 1.0, i64::from(i)).unwrap();
|
||||
/// last = indicator.update(candle);
|
||||
/// }
|
||||
/// assert_eq!(last, Some(1.0));
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TtmTrend {
|
||||
period: usize,
|
||||
sma: Sma,
|
||||
}
|
||||
|
||||
impl TtmTrend {
|
||||
/// Construct a TTM Trend over the given lookback.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::PeriodZero`](crate::error::Error::PeriodZero) if `period == 0`.
|
||||
pub fn new(period: usize) -> Result<Self> {
|
||||
Ok(Self {
|
||||
period,
|
||||
sma: Sma::new(period)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured lookback period.
|
||||
pub const fn period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for TtmTrend {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
let median = f64::midpoint(candle.high, candle.low);
|
||||
let reference = self.sma.update(median)?;
|
||||
Some(if candle.close > reference { 1.0 } else { -1.0 })
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.sma.reset();
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.sma.is_ready()
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"TtmTrend"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::error::Error;
|
||||
use crate::traits::BatchExt;
|
||||
|
||||
fn candle(high: f64, low: f64, close: f64, ts: i64) -> Candle {
|
||||
Candle::new(f64::midpoint(high, low), high, low, close, 1.0, ts).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_zero_period() {
|
||||
assert!(matches!(TtmTrend::new(0), Err(Error::PeriodZero)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let t = TtmTrend::new(6).unwrap();
|
||||
assert_eq!(t.period(), 6);
|
||||
assert_eq!(t.warmup_period(), 6);
|
||||
assert_eq!(t.name(), "TtmTrend");
|
||||
assert!(!t.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn warmup_then_emits() {
|
||||
let mut t = TtmTrend::new(3).unwrap();
|
||||
let candles: Vec<Candle> = (0..3).map(|i| candle(13.0, 9.0, 12.0, i)).collect();
|
||||
let out = t.batch(&candles);
|
||||
assert!(out[0].is_none());
|
||||
assert!(out[1].is_none());
|
||||
assert!(out[2].is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn close_above_reference_is_uptrend() {
|
||||
// Close (12) sits above the median reference (13 + 9) / 2 = 11 -> +1.
|
||||
let mut t = TtmTrend::new(3).unwrap();
|
||||
let candles: Vec<Candle> = (0..6).map(|i| candle(13.0, 9.0, 12.0, i)).collect();
|
||||
assert_eq!(t.batch(&candles).last().unwrap().unwrap(), 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn close_at_or_below_reference_is_downtrend() {
|
||||
// Constant median 10, close equal to the reference -> not strictly above -> -1.
|
||||
let mut t = TtmTrend::new(3).unwrap();
|
||||
let candles: Vec<Candle> = (0..6).map(|i| candle(11.0, 9.0, 10.0, i)).collect();
|
||||
assert_eq!(t.batch(&candles).last().unwrap().unwrap(), -1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut t = TtmTrend::new(3).unwrap();
|
||||
let candles: Vec<Candle> = (0..6).map(|i| candle(13.0, 9.0, 12.0, i)).collect();
|
||||
t.batch(&candles);
|
||||
assert!(t.is_ready());
|
||||
t.reset();
|
||||
assert!(!t.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles: Vec<Candle> = (0..40_i64)
|
||||
.map(|i| {
|
||||
let base = 100.0 + (i as f64 * 0.25).sin() * 4.0;
|
||||
candle(base + 1.0, base - 1.0, base + (i as f64 * 0.5).cos(), i)
|
||||
})
|
||||
.collect();
|
||||
let mut a = TtmTrend::new(6).unwrap();
|
||||
let mut b = TtmTrend::new(6).unwrap();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|c| b.update(*c)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
//! Wave PM — Cynthia Kase's peak-momentum statistic (Wickra reconstruction).
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::indicators::ema::Ema;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Wave PM (Peak Momentum): a `0..100` statistic that rises when the current
|
||||
/// `length`-bar momentum is large relative to its own recent energy — Cynthia
|
||||
/// Kase's gauge of how "peaked" the move is.
|
||||
///
|
||||
/// ```text
|
||||
/// m = close_t - close_{t-length} (length-bar momentum)
|
||||
/// energy = EMA(m^2, length) (mean squared momentum)
|
||||
/// raw = 1 - exp( -m^2 / (2 * energy) ) (0 if energy == 0)
|
||||
/// WavePM = 100 * EMA(raw, smoothing)
|
||||
/// ```
|
||||
///
|
||||
/// The momentum `m` is normalised by its recent variance (`energy`): a move that
|
||||
/// merely matches its typical energy sits at the baseline
|
||||
/// `100·(1 − e^{−1/2}) ≈ 39.35`, while a momentum *spike* that exceeds recent
|
||||
/// energy drives the reading toward `100`. A flat market (`m = 0`) reads `0`.
|
||||
/// High readings mark a peaking, possibly exhausted move rather than a fresh one.
|
||||
///
|
||||
/// Kase's published `WavePM` is platform-specific; this is Wickra's faithful
|
||||
/// reconstruction of its variance-normalised peak-momentum form. The exact
|
||||
/// constants differ from any single vendor implementation, but the shape — flat
|
||||
/// at zero, a fixed baseline on a steady trend, and saturation on an
|
||||
/// acceleration — matches the indicator's intent.
|
||||
///
|
||||
/// Reference: Cynthia Kase, *Trading with the Odds*, 1996 (Wickra reconstruction).
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Indicator, WavePm};
|
||||
///
|
||||
/// let mut indicator = WavePm::new(10, 3).unwrap();
|
||||
/// let mut last = None;
|
||||
/// for i in 0..60 {
|
||||
/// last = indicator.update(100.0 + f64::from(i));
|
||||
/// }
|
||||
/// assert!(last.is_some());
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct WavePm {
|
||||
length: usize,
|
||||
smoothing: usize,
|
||||
closes: VecDeque<f64>,
|
||||
energy_ema: Ema,
|
||||
smooth_ema: Ema,
|
||||
}
|
||||
|
||||
impl WavePm {
|
||||
/// Construct a Wave PM with the momentum `length` and the output `smoothing`
|
||||
/// period.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::PeriodZero`] if `length == 0` or `smoothing == 0`.
|
||||
pub fn new(length: usize, smoothing: usize) -> Result<Self> {
|
||||
if length == 0 {
|
||||
return Err(Error::PeriodZero);
|
||||
}
|
||||
Ok(Self {
|
||||
length,
|
||||
smoothing,
|
||||
closes: VecDeque::with_capacity(length + 1),
|
||||
energy_ema: Ema::new(length)?,
|
||||
smooth_ema: Ema::new(smoothing)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured `(length, smoothing)`.
|
||||
pub const fn periods(&self) -> (usize, usize) {
|
||||
(self.length, self.smoothing)
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for WavePm {
|
||||
type Input = f64;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, close: f64) -> Option<f64> {
|
||||
self.closes.push_back(close);
|
||||
if self.closes.len() > self.length + 1 {
|
||||
self.closes.pop_front();
|
||||
}
|
||||
if self.closes.len() <= self.length {
|
||||
return None;
|
||||
}
|
||||
|
||||
let oldest = *self.closes.front().unwrap_or(&close);
|
||||
let momentum = close - oldest;
|
||||
let energy = self.energy_ema.update(momentum * momentum)?;
|
||||
let raw = if energy <= 0.0 {
|
||||
0.0
|
||||
} else {
|
||||
1.0 - (-(momentum * momentum) / (2.0 * energy)).exp()
|
||||
};
|
||||
self.smooth_ema.update(raw).map(|v| v * 100.0)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.closes.clear();
|
||||
self.energy_ema.reset();
|
||||
self.smooth_ema.reset();
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
2 * self.length + self.smoothing - 1
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.smooth_ema.is_ready()
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"WavePm"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
#[test]
|
||||
fn rejects_zero_period() {
|
||||
assert!(matches!(WavePm::new(0, 3), Err(Error::PeriodZero)));
|
||||
assert!(matches!(WavePm::new(10, 0), Err(Error::PeriodZero)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let w = WavePm::new(10, 3).unwrap();
|
||||
assert_eq!(w.periods(), (10, 3));
|
||||
// 2*10 + 3 - 1 = 22.
|
||||
assert_eq!(w.warmup_period(), 22);
|
||||
assert_eq!(w.name(), "WavePm");
|
||||
assert!(!w.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn warmup_emits_at_expected_bar() {
|
||||
let mut w = WavePm::new(3, 2).unwrap();
|
||||
// warmup = 2*3 + 2 - 1 = 7 -> first value at input 7 (index 6).
|
||||
let inputs: Vec<f64> = (0..12).map(f64::from).collect();
|
||||
let out = w.batch(&inputs);
|
||||
assert!(out[5].is_none());
|
||||
assert!(out[6].is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flat_market_reads_zero() {
|
||||
let mut w = WavePm::new(4, 2).unwrap();
|
||||
let inputs = [50.0; 20];
|
||||
let last = w.batch(&inputs).last().unwrap().unwrap();
|
||||
assert_relative_eq!(last, 0.0, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn steady_trend_reads_baseline() {
|
||||
// Constant-slope ramp: momentum equals its own energy every bar, so the
|
||||
// reading pins to the baseline 100*(1 - e^-0.5).
|
||||
let mut w = WavePm::new(10, 3).unwrap();
|
||||
let inputs: Vec<f64> = (0..60).map(|i| f64::from(i) * 5.0).collect();
|
||||
let last = w.batch(&inputs).last().unwrap().unwrap();
|
||||
let baseline = 100.0 * (1.0 - (-0.5_f64).exp());
|
||||
assert_relative_eq!(last, baseline, epsilon = 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn acceleration_reads_above_baseline() {
|
||||
// A quadratic path: momentum keeps outrunning its lagged energy, so the
|
||||
// reading sits above the steady-trend baseline.
|
||||
let mut w = WavePm::new(10, 3).unwrap();
|
||||
let inputs: Vec<f64> = (0..60).map(|i| f64::from(i * i) * 0.1).collect();
|
||||
let last = w.batch(&inputs).last().unwrap().unwrap();
|
||||
let baseline = 100.0 * (1.0 - (-0.5_f64).exp());
|
||||
assert!(
|
||||
last > baseline,
|
||||
"accelerating wpm {last} should exceed {baseline}"
|
||||
);
|
||||
assert!(last <= 100.0, "wpm {last} must stay <= 100");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut w = WavePm::new(10, 3).unwrap();
|
||||
let inputs: Vec<f64> = (0..60).map(|i| f64::from(i) * 5.0).collect();
|
||||
w.batch(&inputs);
|
||||
assert!(w.is_ready());
|
||||
w.reset();
|
||||
assert!(!w.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let inputs: Vec<f64> = (0..80)
|
||||
.map(|i| 100.0 + (f64::from(i) * 0.2).sin() * 5.0)
|
||||
.collect();
|
||||
let mut a = WavePm::new(10, 3).unwrap();
|
||||
let mut b = WavePm::new(10, 3).unwrap();
|
||||
assert_eq!(
|
||||
a.batch(&inputs),
|
||||
inputs.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -84,59 +84,60 @@ pub use indicators::{
|
||||
FibonacciPivots, FibonacciPivotsOutput, FisherRsi, FisherTransform, FlagPennant, Footprint,
|
||||
FootprintOutput, ForceIndex, FractalChaosBands, FractalChaosBandsOutput, Frama, FundingBasis,
|
||||
FundingRate, FundingRateMean, FundingRateZScore, GainLossRatio, GapSideBySideWhite,
|
||||
GarmanKlassVolatility, Gartley, GeneralizedDema, GeometricMa, GoldenPocket, GoldenPocketOutput,
|
||||
GrangerCausality, GravestoneDoji, Hammer, HangingMan, Harami, HeadAndShoulders, HeikinAshi,
|
||||
HeikinAshiOutput, HiLoActivator, HighLowIndex, HighLowRange, HighWave, Hikkake,
|
||||
HikkakeModified, HilbertDominantCycle, HistoricalVolatility, Hma, HoltWinters, HomingPigeon,
|
||||
HtDcPhase, HtPhasor, HtPhasorOutput, HtTrendMode, HurstChannel, HurstChannelOutput,
|
||||
HurstExponent, Ichimoku, IchimokuOutput, IdenticalThreeCrows, InNeck, Inertia,
|
||||
InformationRatio, InitialBalance, InitialBalanceOutput, InstantaneousTrendline,
|
||||
GarmanKlassVolatility, Gartley, GatorOscillator, GeneralizedDema, GeometricMa, GoldenPocket,
|
||||
GoldenPocketOutput, GrangerCausality, GravestoneDoji, Hammer, HangingMan, Harami,
|
||||
HeadAndShoulders, HeikinAshi, HeikinAshiOutput, HiLoActivator, HighLowIndex, HighLowRange,
|
||||
HighWave, Hikkake, HikkakeModified, HilbertDominantCycle, HistoricalVolatility, Hma,
|
||||
HoltWinters, HomingPigeon, HtDcPhase, HtPhasor, HtPhasorOutput, HtTrendMode, HurstChannel,
|
||||
HurstChannelOutput, HurstExponent, Ichimoku, IchimokuOutput, IdenticalThreeCrows, InNeck,
|
||||
Inertia, InformationRatio, InitialBalance, InitialBalanceOutput, InstantaneousTrendline,
|
||||
IntradayMomentumIndex, IntradayVolatilityProfile, IntradayVolatilityProfileOutput,
|
||||
InverseFisherTransform, InvertedHammer, Jma, JumpIndicator, KagiBars, KalmanHedgeRatio,
|
||||
KalmanHedgeRatioOutput, Kama, KellyCriterion, Keltner, KeltnerOutput, Kicking, KickingByLength,
|
||||
Kst, KstOutput, Kurtosis, Kvo, KylesLambda, LadderBottom, LaguerreRsi, LeadLagCrossCorrelation,
|
||||
LeadLagCrossCorrelationOutput, LinRegAngle, LinRegChannel, LinRegChannelOutput,
|
||||
LinRegIntercept, LinRegSlope, LinearRegression, LiquidationFeatures, LiquidationFeaturesOutput,
|
||||
LogReturn, LongLeggedDoji, LongLine, LongShortRatio, MaEnvelope, MaEnvelopeOutput, MacdExt,
|
||||
MacdFix, MacdIndicator, MacdOutput, Mama, MamaOutput, MarketFacilitationIndex, Marubozu,
|
||||
MassIndex, MatHold, MatchingLow, MaxDrawdown, McClellanOscillator, McClellanSummationIndex,
|
||||
McGinleyDynamic, MedianAbsoluteDeviation, MedianMa, MedianPrice, Mfi, Microprice, MidPoint,
|
||||
MidPrice, MinusDi, MinusDm, Mom, MorningDojiStar, MorningEveningStar, Natr, NewHighsNewLows,
|
||||
Nvi, OIPriceDivergence, OIWeighted, Obv, OmegaRatio, OnNeck, OpenInterestDelta,
|
||||
OpeningMarubozu, OpeningRange, OpeningRangeOutput, OrderBookImbalanceFull,
|
||||
OrderBookImbalanceTop1, OrderBookImbalanceTopN, OrderFlowImbalance, OuHalfLife, OvernightGap,
|
||||
OvernightIntradayReturn, OvernightIntradayReturnOutput, PainIndex, PairSpreadZScore,
|
||||
PairwiseBeta, ParkinsonVolatility, PearsonCorrelation, PercentAboveMa, PercentB,
|
||||
PercentageTrailingStop, Pgo, PiercingDarkCloud, PlusDi, PlusDm, Pmo, PointAndFigureBars, Ppo,
|
||||
ProfitFactor, Psar, Pvi, Qqe, QqeOutput, QuotedSpread, RSquared, RealizedSpread,
|
||||
RealizedVolatility, RecoveryFactor, RectangleRange, RegimeLabel, RelativeStrengthAB,
|
||||
RelativeStrengthOutput, RenkoBars, RenkoTrailingStop, RickshawMan, RisingThreeMethods, Rmi,
|
||||
Roc, Rocp, Rocr, Rocr100, RogersSatchellVolatility, RollMeasure, RollingCorrelation,
|
||||
RollingCovariance, RollingIqr, RollingPercentileRank, RollingQuantile, RollingVwap,
|
||||
RoofingFilter, Rsi, Rsx, Rvi, RviVolatility, Rwi, RwiOutput, SarExt, SeasonalZScore,
|
||||
SeparatingLines, SessionHighLow, SessionHighLowOutput, SessionRange, SessionRangeOutput,
|
||||
SessionVwap, Shark, SharpeRatio, ShootingStar, ShortLine, SignedVolume, SineWave,
|
||||
SineWeightedMa, Skewness, Sma, Smi, Smma, SortinoRatio, SpearmanCorrelation, SpinningTop,
|
||||
SpreadAr1Coefficient, SpreadBollingerBands, SpreadBollingerBandsOutput, SpreadHurst,
|
||||
StalledPattern, StandardError, StandardErrorBands, StandardErrorBandsOutput, StarcBands,
|
||||
StarcBandsOutput, Stc, StdDev, StepTrailingStop, StickSandwich, StochRsi, Stochastic,
|
||||
StochasticCci, StochasticOutput, SuperSmoother, SuperTrend, SuperTrendOutput,
|
||||
TakerBuySellRatio, Takuri, TasukiGap, TdCombo, TdCountdown, TdDeMarker, TdDifferential,
|
||||
TdLines, TdLinesOutput, TdOpen, TdPressure, TdRangeProjection, TdRangeProjectionOutput, TdRei,
|
||||
TdRiskLevel, TdRiskLevelOutput, TdSequential, TdSequentialOutput, TdSetup, Tema,
|
||||
TermStructureBasis, ThreeDrives, ThreeInside, ThreeLineStrike, ThreeOutside,
|
||||
ThreeSoldiersOrCrows, ThreeStarsInSouth, Thrusting, TickIndex, Tii, TimeOfDayReturnProfile,
|
||||
TimeOfDayReturnProfileOutput, TpoProfile, TpoProfileOutput, TradeImbalance, TrendLabel,
|
||||
TreynorRatio, Triangle, Trima, Trin, TripleTopBottom, Trix, TrueRange, Tsf, Tsi, Tsv,
|
||||
TtmSqueeze, TtmSqueezeOutput, TurnOfMonth, Tweezer, TwoCrows, TypicalPrice, UlcerIndex,
|
||||
UltimateOscillator, UniqueThreeRiver, UpDownVolumeRatio, UpsideGapThreeMethods,
|
||||
UpsideGapTwoCrows, ValueArea, ValueAreaOutput, ValueAtRisk, Variance, VarianceRatio,
|
||||
VerticalHorizontalFilter, Vidya, VoltyStop, VolumeByTimeProfile, VolumeByTimeProfileOutput,
|
||||
VolumeOscillator, VolumePriceTrend, VolumeProfile, VolumeProfileOutput, Vortex, VortexOutput,
|
||||
Vpin, Vwap, VwapStdDevBands, VwapStdDevBandsOutput, Vwma, Vzo, WaveTrend, WaveTrendOutput,
|
||||
Wedge, WeightedClose, WickRatio, WilliamsFractals, WilliamsFractalsOutput, WilliamsR, WinRate,
|
||||
Wma, WoodiePivots, WoodiePivotsOutput, YangZhangVolatility, YoyoExit, ZScore, ZeroLagMacd,
|
||||
ZeroLagMacdOutput, ZigZag, ZigZagOutput, Zlema, FAMILIES, T3,
|
||||
KalmanHedgeRatioOutput, Kama, KasePermissionStochastic, KellyCriterion, Keltner, KeltnerOutput,
|
||||
Kicking, KickingByLength, Kst, KstOutput, Kurtosis, Kvo, KylesLambda, LadderBottom,
|
||||
LaguerreRsi, LeadLagCrossCorrelation, LeadLagCrossCorrelationOutput, LinRegAngle,
|
||||
LinRegChannel, LinRegChannelOutput, LinRegIntercept, LinRegSlope, LinearRegression,
|
||||
LiquidationFeatures, LiquidationFeaturesOutput, LogReturn, LongLeggedDoji, LongLine,
|
||||
LongShortRatio, MaEnvelope, MaEnvelopeOutput, MacdExt, MacdFix, MacdIndicator, MacdOutput,
|
||||
Mama, MamaOutput, MarketFacilitationIndex, Marubozu, MassIndex, MatHold, MatchingLow,
|
||||
MaxDrawdown, McClellanOscillator, McClellanSummationIndex, McGinleyDynamic,
|
||||
MedianAbsoluteDeviation, MedianMa, MedianPrice, Mfi, Microprice, MidPoint, MidPrice, MinusDi,
|
||||
MinusDm, Mom, MorningDojiStar, MorningEveningStar, Natr, NewHighsNewLows, Nvi,
|
||||
OIPriceDivergence, OIWeighted, Obv, OmegaRatio, OnNeck, OpenInterestDelta, OpeningMarubozu,
|
||||
OpeningRange, OpeningRangeOutput, OrderBookImbalanceFull, OrderBookImbalanceTop1,
|
||||
OrderBookImbalanceTopN, OrderFlowImbalance, OuHalfLife, OvernightGap, OvernightIntradayReturn,
|
||||
OvernightIntradayReturnOutput, PainIndex, PairSpreadZScore, PairwiseBeta, ParkinsonVolatility,
|
||||
PearsonCorrelation, PercentAboveMa, PercentB, PercentageTrailingStop, Pgo, PiercingDarkCloud,
|
||||
PlusDi, PlusDm, Pmo, PointAndFigureBars, PolarizedFractalEfficiency, Ppo, 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, Tsi, Tsv, TtmSqueeze, TtmSqueezeOutput, TtmTrend,
|
||||
TurnOfMonth, Tweezer, TwoCrows, TypicalPrice, UlcerIndex, UltimateOscillator, UniqueThreeRiver,
|
||||
UpDownVolumeRatio, UpsideGapThreeMethods, UpsideGapTwoCrows, ValueArea, ValueAreaOutput,
|
||||
ValueAtRisk, Variance, VarianceRatio, VerticalHorizontalFilter, Vidya, VoltyStop,
|
||||
VolumeByTimeProfile, VolumeByTimeProfileOutput, VolumeOscillator, VolumePriceTrend,
|
||||
VolumeProfile, VolumeProfileOutput, Vortex, VortexOutput, Vpin, Vwap, VwapStdDevBands,
|
||||
VwapStdDevBandsOutput, Vwma, Vzo, WavePm, WaveTrend, WaveTrendOutput, Wedge, WeightedClose,
|
||||
WickRatio, WilliamsFractals, WilliamsFractalsOutput, WilliamsR, WinRate, Wma, WoodiePivots,
|
||||
WoodiePivotsOutput, YangZhangVolatility, YoyoExit, ZScore, ZeroLagMacd, ZeroLagMacdOutput,
|
||||
ZigZag, ZigZagOutput, Zlema, FAMILIES, T3,
|
||||
};
|
||||
// `FootprintLevel` is a row element of `FootprintOutput`, re-exported on its own
|
||||
// line so the indicator-count tooling (which scans the braced block above and
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ That includes:
|
||||
[Python](https://docs.wickra.org/Quickstart-Python),
|
||||
[Node](https://docs.wickra.org/Quickstart-Node), and
|
||||
[WASM](https://docs.wickra.org/Quickstart-WASM).
|
||||
- A per-indicator deep dive for every one of the **413 indicators** across
|
||||
- A per-indicator deep dive for every one of the **420 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 &
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
//! `Ema(20)`. This target now covers every scalar indicator in the catalogue.
|
||||
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
use wickra_core::{AdaptiveCycle, AdaptiveLaguerreFilter, Alma, AnchoredRsi, Apo, Autocorrelation, AverageDrawdown, BatchExt, Beta, BollingerBands, CalmarRatio, CenterOfGravity, Cfo, Cmo, CoefficientOfVariation, ConditionalValueAtRisk, ConnorsRsi, Coppock, CyberneticCycle, Decycler, DecyclerOscillator, Dema, DerivativeOscillator, DetrendedStdDev, DisparityIndex, DoubleBollinger, Dpo, DrawdownDuration, DynamicMomentumIndex, EhlersStochastic, Ehma, ElderImpulse, Ema, EmpiricalModeDecomposition, Expectancy, Fama, FisherRsi, FisherTransform, Frama, GainLossRatio, GeneralizedDema, GeometricMa, HilbertDominantCycle, HistoricalVolatility, Hma, HoltWinters, HtDcPhase, HtPhasor, HtTrendMode, HurstExponent, Indicator, InstantaneousTrendline, InverseFisherTransform, Jma, JumpIndicator, Kama, KellyCriterion, Kst, Kurtosis, LaguerreRsi, LinRegAngle, LinRegChannel, LinRegIntercept, LinRegSlope, LinearRegression, LogReturn, MaEnvelope, MaType, MacdExt, MacdFix, MacdIndicator, Mama, MaxDrawdown, McGinleyDynamic, MedianAbsoluteDeviation, MedianMa, MidPoint, Mom, OmegaRatio, PainIndex, PearsonCorrelation, PercentageTrailingStop, Pmo, Ppo, ProfitFactor, Qqe, RSquared, RealizedVolatility, RecoveryFactor, RegimeLabel, RenkoTrailingStop, Rmi, Roc, Rocp, Rocr, Rocr100, RollingIqr, RollingPercentileRank, RollingQuantile, RoofingFilter, Rsi, Rsx, RviVolatility, SharpeRatio, SineWave, SineWeightedMa, Skewness, Sma, Smma, SortinoRatio, SpearmanCorrelation, StandardError, StandardErrorBands, Stc, StdDev, StepTrailingStop, StochRsi, SuperSmoother, Tema, Tii, TrendLabel, Trima, Trix, Tsf, Tsi, UlcerIndex, ValueAtRisk, Variance, VerticalHorizontalFilter, Vidya, WinRate, Wma, ZScore, ZeroLagMacd, Zlema, T3};
|
||||
use wickra_core::{AdaptiveCycle, AdaptiveLaguerreFilter, Alma, AnchoredRsi, Apo, Autocorrelation, AverageDrawdown, BatchExt, Beta, BollingerBands, CalmarRatio, CenterOfGravity, Cfo, Cmo, CoefficientOfVariation, ConditionalValueAtRisk, ConnorsRsi, Coppock, CyberneticCycle, Decycler, DecyclerOscillator, Dema, DerivativeOscillator, DetrendedStdDev, DisparityIndex, DoubleBollinger, Dpo, DrawdownDuration, DynamicMomentumIndex, EhlersStochastic, Ehma, ElderImpulse, Ema, EmpiricalModeDecomposition, Expectancy, Fama, FisherRsi, FisherTransform, Frama, GainLossRatio, GeneralizedDema, GeometricMa, HilbertDominantCycle, HistoricalVolatility, Hma, HoltWinters, HtDcPhase, HtPhasor, HtTrendMode, HurstExponent, Indicator, InstantaneousTrendline, InverseFisherTransform, Jma, JumpIndicator, Kama, KellyCriterion, Kst, Kurtosis, LaguerreRsi, LinRegAngle, LinRegChannel, LinRegIntercept, LinRegSlope, LinearRegression, LogReturn, MaEnvelope, MaType, MacdExt, MacdFix, MacdIndicator, Mama, MaxDrawdown, McGinleyDynamic, MedianAbsoluteDeviation, MedianMa, MidPoint, Mom, OmegaRatio, PainIndex, PearsonCorrelation, PercentageTrailingStop, Pmo, PolarizedFractalEfficiency, Ppo, ProfitFactor, Qqe, RSquared, RealizedVolatility, RecoveryFactor, RegimeLabel, RenkoTrailingStop, Rmi, Roc, Rocp, Rocr, Rocr100, RollingIqr, RollingPercentileRank, RollingQuantile, RoofingFilter, Rsi, Rsx, RviVolatility, SharpeRatio, SineWave, SineWeightedMa, Skewness, Sma, Smma, SortinoRatio, SpearmanCorrelation, StandardError, StandardErrorBands, Stc, StdDev, StepTrailingStop, StochRsi, SuperSmoother, Tema, Tii, TrendLabel, TrendStrengthIndex, Trima, Trix, Tsf, Tsi, UlcerIndex, ValueAtRisk, Variance, VerticalHorizontalFilter, Vidya, WavePm, WinRate, Wma, ZScore, ZeroLagMacd, Zlema, T3};
|
||||
|
||||
/// Drive a single streaming + batch run through one scalar indicator. Marked
|
||||
/// `#[inline(never)]` so a panic backtrace pin-points the specific indicator.
|
||||
@@ -73,6 +73,9 @@ fuzz_target!(|data: Vec<f64>| {
|
||||
drive(|| DynamicMomentumIndex::new(14).unwrap(), &data);
|
||||
drive(|| Rmi::new(14, 5).unwrap(), &data);
|
||||
drive(|| DerivativeOscillator::new(14, 5, 3, 9).unwrap(), &data);
|
||||
drive(|| TrendStrengthIndex::new(20).unwrap(), &data);
|
||||
drive(|| PolarizedFractalEfficiency::new(10, 5).unwrap(), &data);
|
||||
drive(|| WavePm::new(32, 3).unwrap(), &data);
|
||||
drive(|| Tsi::new(25, 13).unwrap(), &data);
|
||||
drive(|| Pmo::new(35, 20).unwrap(), &data);
|
||||
drive(|| Tii::new(60, 30).unwrap(), &data);
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
//! WeightedClose.
|
||||
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
use wickra_core::{AbandonedBaby, Abcd, AccelerationBands, AcceleratorOscillator, AdOscillator, Adl, AdvanceBlock, Adx, Adxr, Alligator, AnchoredVwap, Aroon, AroonOscillator, Atr, AtrBands, AtrTrailingStop, AutoFib, AverageDailyRange, AwesomeOscillator, AwesomeOscillatorHistogram, BalanceOfPower, Bat, BatchExt, BeltHold, BodySizePct, Breakaway, Butterfly, Camarilla, Candle, Cci, ChaikinMoneyFlow, ChaikinOscillator, ChaikinVolatility, ChandeKrollStop, ChandelierExit, ChoppinessIndex, ClassicPivots, CloseVsOpen, ClosingMarubozu, ConcealingBabySwallow, Counterattack, Crab, CupAndHandle, Cypher, DayOfWeekProfile, DemandIndex, DemarkPivots, Doji, DojiStar, Donchian, DonchianStop, DoubleTopBottom, DownsideGapThreeMethods, DragonflyDoji, Dx, EaseOfMovement, ElderRay, Engulfing, EveningDojiStar, Evwma, FallingThreeMethods, FibArcs, FibChannel, FibConfluence, FibExtension, FibFan, FibProjection, FibRetracement, FibTimeZones, FibonacciPivots, FlagPennant, ForceIndex, FractalChaosBands, GapSideBySideWhite, GarmanKlassVolatility, Gartley, GoldenPocket, GravestoneDoji, Hammer, HangingMan, Harami, HeadAndShoulders, HeikinAshi, HiLoActivator, HighLowRange, HighWave, Hikkake, HikkakeModified, HomingPigeon, HurstChannel, Ichimoku, IdenticalThreeCrows, InNeck, Indicator, Inertia, InitialBalance, IntradayMomentumIndex, IntradayVolatilityProfile, InvertedHammer, Keltner, Kicking, KickingByLength, Kvo, LadderBottom, LongLeggedDoji, LongLine, MarketFacilitationIndex, Marubozu, MassIndex, MatHold, MatchingLow, AvgPrice, MedianPrice, Mfi, MidPrice, MinusDi, MinusDm, MorningDojiStar, MorningEveningStar, Natr, Nvi, Obv, OnNeck, OpeningMarubozu, OpeningRange, OvernightGap, OvernightIntradayReturn, ParkinsonVolatility, Pgo, PiercingDarkCloud, PlusDi, PlusDm, Psar, Pvi, RectangleRange, RickshawMan, RisingThreeMethods, RogersSatchellVolatility, RollingVwap, Rvi, Rwi, SarExt, SeasonalZScore, SeparatingLines, SessionHighLow, SessionRange, SessionVwap, Shark, ShootingStar, ShortLine, Smi, SpinningTop, StalledPattern, StarcBands, StickSandwich, Stochastic, StochasticCci, SuperTrend, Takuri, TasukiGap, TdCombo, TdCountdown, TdDeMarker, TdDifferential, TdLines, TdOpen, TdPressure, TdRangeProjection, TdRei, TdRiskLevel, TdSequential, TdSetup, ThreeDrives, ThreeInside, ThreeLineStrike, ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth, Thrusting, TimeOfDayReturnProfile, TpoProfile, Triangle, TripleTopBottom, TrueRange, Tsv, TtmSqueeze, TurnOfMonth, Tweezer, TwoCrows, TypicalPrice, UltimateOscillator, UniqueThreeRiver, UpsideGapThreeMethods, UpsideGapTwoCrows, ValueArea, VoltyStop, VolumeByTimeProfile, VolumeOscillator, VolumePriceTrend, VolumeProfile, Vortex, Vwap, VwapStdDevBands, Vwma, Vzo, WaveTrend, Wedge, WeightedClose, WickRatio, WilliamsFractals, WilliamsR, WoodiePivots, YangZhangVolatility, YoyoExit, ZigZag};
|
||||
use wickra_core::{AbandonedBaby, Abcd, AccelerationBands, AcceleratorOscillator, AdOscillator, Adl, AdvanceBlock, Adx, Adxr, Alligator, AnchoredVwap, Aroon, AroonOscillator, Atr, AtrBands, AtrTrailingStop, AutoFib, AverageDailyRange, AwesomeOscillator, AwesomeOscillatorHistogram, BalanceOfPower, Bat, BatchExt, BeltHold, BodySizePct, Breakaway, Butterfly, Camarilla, Candle, Cci, ChaikinMoneyFlow, ChaikinOscillator, ChaikinVolatility, ChandeKrollStop, ChandelierExit, ChoppinessIndex, ClassicPivots, CloseVsOpen, ClosingMarubozu, ConcealingBabySwallow, Counterattack, Crab, CupAndHandle, Cypher, DayOfWeekProfile, DemandIndex, DemarkPivots, Doji, DojiStar, Donchian, DonchianStop, DoubleTopBottom, DownsideGapThreeMethods, DragonflyDoji, Dx, EaseOfMovement, ElderRay, Engulfing, EveningDojiStar, Evwma, FallingThreeMethods, FibArcs, FibChannel, FibConfluence, FibExtension, FibFan, FibProjection, FibRetracement, FibTimeZones, FibonacciPivots, FlagPennant, ForceIndex, FractalChaosBands, GapSideBySideWhite, GarmanKlassVolatility, Gartley, GatorOscillator, GoldenPocket, GravestoneDoji, Hammer, HangingMan, Harami, HeadAndShoulders, HeikinAshi, HiLoActivator, HighLowRange, HighWave, Hikkake, HikkakeModified, HomingPigeon, HurstChannel, Ichimoku, IdenticalThreeCrows, InNeck, Indicator, Inertia, InitialBalance, IntradayMomentumIndex, IntradayVolatilityProfile, InvertedHammer, KasePermissionStochastic, Keltner, Kicking, KickingByLength, Kvo, LadderBottom, LongLeggedDoji, LongLine, MarketFacilitationIndex, Marubozu, MassIndex, MatHold, MatchingLow, AvgPrice, MedianPrice, Mfi, MidPrice, MinusDi, MinusDm, MorningDojiStar, MorningEveningStar, Natr, Nvi, Obv, OnNeck, OpeningMarubozu, OpeningRange, OvernightGap, OvernightIntradayReturn, ParkinsonVolatility, Pgo, PiercingDarkCloud, PlusDi, PlusDm, Psar, Pvi, Qstick, RectangleRange, RickshawMan, RisingThreeMethods, RogersSatchellVolatility, RollingVwap, Rvi, Rwi, SarExt, SeasonalZScore, SeparatingLines, SessionHighLow, SessionRange, SessionVwap, Shark, ShootingStar, ShortLine, Smi, SpinningTop, StalledPattern, StarcBands, StickSandwich, Stochastic, StochasticCci, SuperTrend, Takuri, TasukiGap, TdCombo, TdCountdown, TdDeMarker, TdDifferential, TdLines, TdOpen, TdPressure, TdRangeProjection, TdRei, TdRiskLevel, TdSequential, TdSetup, ThreeDrives, ThreeInside, ThreeLineStrike, ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth, Thrusting, TimeOfDayReturnProfile, TpoProfile, Triangle, TripleTopBottom, TrueRange, Tsv, TtmSqueeze, TtmTrend, TurnOfMonth, Tweezer, TwoCrows, TypicalPrice, UltimateOscillator, UniqueThreeRiver, UpsideGapThreeMethods, UpsideGapTwoCrows, ValueArea, VoltyStop, VolumeByTimeProfile, VolumeOscillator, VolumePriceTrend, VolumeProfile, Vortex, Vwap, VwapStdDevBands, Vwma, Vzo, WaveTrend, Wedge, WeightedClose, WickRatio, WilliamsFractals, WilliamsR, WoodiePivots, YangZhangVolatility, YoyoExit, ZigZag};
|
||||
|
||||
/// Convert a flat `f64` stream into a `Vec<Candle>` by chunking it into
|
||||
/// `[open, high, low, close, volume]` groups. Tuples that fail OHLCV
|
||||
@@ -87,6 +87,10 @@ fuzz_target!(|data: Vec<f64>| {
|
||||
drive(|| YoyoExit::new(14, 2.0).unwrap(), &candles);
|
||||
|
||||
// --- Trend & Directional ---
|
||||
drive(|| KasePermissionStochastic::new(9, 3).unwrap(), &candles);
|
||||
drive(|| GatorOscillator::new(13, 8, 5).unwrap(), &candles);
|
||||
drive(|| Qstick::new(10).unwrap(), &candles);
|
||||
drive(|| TtmTrend::new(6).unwrap(), &candles);
|
||||
drive(|| Adx::new(14).unwrap(), &candles);
|
||||
drive(|| Adxr::new(14).unwrap(), &candles);
|
||||
drive(|| PlusDm::new(14).unwrap(), &candles);
|
||||
|
||||
Reference in New Issue
Block a user