feat: add Anchored RSI to the momentum oscillators family (#144)
Cumulative Relative Strength Index whose averaging begins at a runtime-chosen anchor bar (set_anchor), the momentum counterpart to Anchored VWAP. Scalar f64 input, 0..=100 output; wired through core, Python, Node and WASM bindings, fuzz, benches, tests and docs. Indicator count 289 -> 290.
This commit is contained in:
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Anchored RSI** — a cumulative Relative Strength Index whose averaging begins at a runtime-chosen anchor bar (`set_anchor`), the momentum counterpart to Anchored VWAP. Every up- and down-move since the anchor is weighted equally, so it reports the RSI of the entire move since the anchor point. Scalar input, Momentum Oscillators family; available in Rust, Python, Node and WASM.
|
||||||
|
|
||||||
## [0.4.4] - 2026-06-02
|
## [0.4.4] - 2026-06-02
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://wickra.org"><img src="https://raw.githubusercontent.com/wickra-lib/.github/main/profile/wickra-banner.webp?v=289" 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=290" alt="Wickra — streaming-first technical indicators" width="100%"></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
[](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml)
|
[](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml)
|
||||||
@@ -47,7 +47,7 @@ Full documentation lives at **[docs.wickra.org](https://docs.wickra.org)**:
|
|||||||
[Node](https://docs.wickra.org/Quickstart-Node),
|
[Node](https://docs.wickra.org/Quickstart-Node),
|
||||||
[WASM](https://docs.wickra.org/Quickstart-WASM).
|
[WASM](https://docs.wickra.org/Quickstart-WASM).
|
||||||
- **Indicators** — a per-indicator deep dive (formula, parameters, warmup) for
|
- **Indicators** — a per-indicator deep dive (formula, parameters, warmup) for
|
||||||
every one of the 289 indicators; start at the
|
every one of the 290 indicators; start at the
|
||||||
[indicators overview](https://docs.wickra.org/Indicators-Overview).
|
[indicators overview](https://docs.wickra.org/Indicators-Overview).
|
||||||
- **Reference** — [warmup periods](https://docs.wickra.org/Warmup-Periods),
|
- **Reference** — [warmup periods](https://docs.wickra.org/Warmup-Periods),
|
||||||
[streaming vs batch](https://docs.wickra.org/Streaming-vs-Batch),
|
[streaming vs batch](https://docs.wickra.org/Streaming-vs-Batch),
|
||||||
@@ -135,7 +135,7 @@ python -m benchmarks.compare_libraries
|
|||||||
|
|
||||||
## Indicators
|
## Indicators
|
||||||
|
|
||||||
289 streaming-first indicators across eighteen families. Every one passes the
|
290 streaming-first indicators across eighteen families. Every one passes the
|
||||||
`batch == streaming` equivalence test, reference-value tests, and reset
|
`batch == streaming` equivalence test, reference-value tests, and reset
|
||||||
semantics tests. Each has a per-indicator deep dive (formula, parameters,
|
semantics tests. Each has a per-indicator deep dive (formula, parameters,
|
||||||
warmup) at [docs.wickra.org](https://docs.wickra.org/Indicators-Overview).
|
warmup) at [docs.wickra.org](https://docs.wickra.org/Indicators-Overview).
|
||||||
@@ -143,7 +143,7 @@ warmup) at [docs.wickra.org](https://docs.wickra.org/Indicators-Overview).
|
|||||||
| Family | Indicators |
|
| Family | Indicators |
|
||||||
|--------|-----------|
|
|--------|-----------|
|
||||||
| Moving Averages | SMA, EMA, WMA, DEMA, TEMA, HMA, KAMA, SMMA, TRIMA, ZLEMA, T3, VWMA, ALMA, McGinley Dynamic, FRAMA, VIDYA, JMA, Alligator, EVWMA |
|
| Moving Averages | SMA, EMA, WMA, DEMA, TEMA, HMA, KAMA, SMMA, TRIMA, ZLEMA, T3, VWMA, ALMA, McGinley Dynamic, FRAMA, VIDYA, JMA, Alligator, EVWMA |
|
||||||
| Momentum Oscillators | RSI (Wilder), Stochastic, CCI, ROC, Williams %R, MFI, Awesome Oscillator, MOM, CMO, TSI, PMO, StochRSI, Ultimate Oscillator, RVI, PGO, KST, SMI, Laguerre RSI, Connors RSI, Inertia |
|
| 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 |
|
||||||
| Trend & Directional | MACD, 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 |
|
| Trend & Directional | MACD, 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 |
|
||||||
| Price Oscillators | PPO, DPO, Coppock, Accelerator Oscillator, Balance of Power, APO, AO Histogram, CFO, Zero-Lag MACD, Elder Impulse, STC |
|
| Price Oscillators | PPO, DPO, Coppock, Accelerator Oscillator, Balance of Power, APO, AO Histogram, CFO, Zero-Lag MACD, Elder Impulse, STC |
|
||||||
| Volatility & Bands | ATR, Bollinger Bands, Keltner Channels, Donchian Channels, NATR, StdDev, Ulcer Index, Historical Volatility, Bollinger Bandwidth, %B, True Range, Chaikin Volatility, RVI (Relative Volatility Index), Parkinson Volatility, Garman-Klass Volatility, Rogers-Satchell Volatility, Yang-Zhang Volatility |
|
| Volatility & Bands | ATR, Bollinger Bands, Keltner Channels, Donchian Channels, NATR, StdDev, Ulcer Index, Historical Volatility, Bollinger Bandwidth, %B, True Range, Chaikin Volatility, RVI (Relative Volatility Index), Parkinson Volatility, Garman-Klass Volatility, Rogers-Satchell Volatility, Yang-Zhang Volatility |
|
||||||
@@ -238,7 +238,7 @@ A Python live-trading example using the public `websockets` package lives at
|
|||||||
```
|
```
|
||||||
wickra/
|
wickra/
|
||||||
├── crates/
|
├── crates/
|
||||||
│ ├── wickra-core/ core engine + all 289 indicators
|
│ ├── wickra-core/ core engine + all 290 indicators
|
||||||
│ ├── wickra/ top-level facade crate (publishes on crates.io) + benches/
|
│ ├── wickra/ top-level facade crate (publishes on crates.io) + benches/
|
||||||
│ └── wickra-data/ CSV reader, tick aggregator, live exchange feeds
|
│ └── wickra-data/ CSV reader, tick aggregator, live exchange feeds
|
||||||
├── bindings/
|
├── bindings/
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ const scalarFactories = {
|
|||||||
EMA: () => new wickra.EMA(14),
|
EMA: () => new wickra.EMA(14),
|
||||||
WMA: () => new wickra.WMA(14),
|
WMA: () => new wickra.WMA(14),
|
||||||
RSI: () => new wickra.RSI(14),
|
RSI: () => new wickra.RSI(14),
|
||||||
|
AnchoredRSI: () => new wickra.AnchoredRSI(),
|
||||||
DEMA: () => new wickra.DEMA(10),
|
DEMA: () => new wickra.DEMA(10),
|
||||||
TEMA: () => new wickra.TEMA(10),
|
TEMA: () => new wickra.TEMA(10),
|
||||||
HMA: () => new wickra.HMA(9),
|
HMA: () => new wickra.HMA(9),
|
||||||
|
|||||||
Vendored
+10
@@ -1224,6 +1224,16 @@ export declare class WilliamsAD {
|
|||||||
isReady(): boolean
|
isReady(): boolean
|
||||||
warmupPeriod(): number
|
warmupPeriod(): number
|
||||||
}
|
}
|
||||||
|
export type AnchoredRsiNode = AnchoredRSI
|
||||||
|
export declare class AnchoredRSI {
|
||||||
|
constructor()
|
||||||
|
setAnchor(): void
|
||||||
|
update(value: number): number | null
|
||||||
|
batch(prices: Array<number>): Array<number>
|
||||||
|
reset(): void
|
||||||
|
isReady(): boolean
|
||||||
|
warmupPeriod(): number
|
||||||
|
}
|
||||||
export type AnchoredVwapNode = AnchoredVWAP
|
export type AnchoredVwapNode = AnchoredVWAP
|
||||||
export declare class AnchoredVWAP {
|
export declare class AnchoredVWAP {
|
||||||
constructor()
|
constructor()
|
||||||
|
|||||||
@@ -310,7 +310,7 @@ if (!nativeBinding) {
|
|||||||
throw new Error(`Failed to load native binding`)
|
throw new Error(`Failed to load native binding`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const { version, SMA, EMA, WMA, RSI, DEMA, TEMA, HMA, ROC, TRIX, SMMA, TRIMA, ZLEMA, MOM, CMO, DPO, StdDev, UlcerIndex, VerticalHorizontalFilter, ZScore, McGinleyDynamic, FRAMA, SuperSmoother, FisherTransform, Decycler, CenterOfGravity, CyberneticCycle, InstantaneousTrendline, EhlersStochastic, RVIVolatility, Variance, CoefficientOfVariation, Skewness, Kurtosis, StandardError, DetrendedStdDev, RSquared, MedianAbsoluteDeviation, Autocorrelation, HurstExponent, PearsonCorrelation, Beta, PairwiseBeta, SpearmanCorrelation, PairSpreadZScore, LeadLagCrossCorrelation, Cointegration, RelativeStrengthAB, MACD, BollingerBands, ATR, Stochastic, OBV, ADX, ADXR, CCI, WilliamsR, MFI, PSAR, Keltner, Donchian, VWAP, RollingVWAP, AwesomeOscillator, Aroon, Inertia, ConnorsRSI, LaguerreRSI, SMI, KST, PGO, RVI, AwesomeOscillatorHistogram, STC, ElderImpulse, ZeroLagMACD, CFO, APO, KAMA, EVWMA, Alligator, JMA, VIDYA, ALMA, T3, TSI, PMO, TII, ADL, VolumePriceTrend, ChaikinMoneyFlow, ChaikinOscillator, ForceIndex, NVI, PVI, VolumeOscillator, KVO, WilliamsAD, AnchoredVWAP, DemandIndex, TSV, VZO, MarketFacilitationIndex, EaseOfMovement, SuperTrend, ChandelierExit, ChandeKrollStop, AtrTrailingStop, HiLoActivator, VoltyStop, YoyoExit, DonchianStop, PercentageTrailingStop, StepTrailingStop, RenkoTrailingStop, TypicalPrice, MedianPrice, WeightedClose, LinearRegression, LinRegSlope, AcceleratorOscillator, BalanceOfPower, ChoppinessIndex, TrueRange, ChaikinVolatility, YangZhangVolatility, RogersSatchellVolatility, GarmanKlassVolatility, ParkinsonVolatility, LinRegAngle, BollingerBandwidth, PercentB, NATR, HistoricalVolatility, AroonOscillator, WaveTrend, RWI, Vortex, MassIndex, StochRSI, UltimateOscillator, PPO, Coppock, VWMA, MaEnvelope, AccelerationBands, StarcBands, AtrBands, HurstChannel, LinRegChannel, StandardErrorBands, DoubleBollinger, TtmSqueeze, FractalChaosBands, VwapStdDevBands, ClassicPivots, FibonacciPivots, Camarilla, WoodiePivots, DemarkPivots, WilliamsFractals, ZigZag, TDSetup, TDSequential, TDDeMarker, TDREI, TDPressure, TDCombo, TDCountdown, TDLines, TDRangeProjection, TDDifferential, TDOpen, TDRiskLevel, InverseFisherTransform, DecyclerOscillator, RoofingFilter, EmpiricalModeDecomposition, HilbertDominantCycle, AdaptiveCycle, SineWave, MAMA, FAMA, Ichimoku, HeikinAshi, ValueArea, InitialBalance, OpeningRange, Doji, Hammer, InvertedHammer, HangingMan, ShootingStar, Engulfing, Harami, MorningEveningStar, ThreeSoldiersOrCrows, PiercingDarkCloud, Marubozu, Tweezer, SpinningTop, ThreeInside, ThreeOutside, TwoCrows, UpsideGapTwoCrows, IdenticalThreeCrows, ThreeLineStrike, ThreeStarsInSouth, AbandonedBaby, AdvanceBlock, BeltHold, Breakaway, Counterattack, DojiStar, DragonflyDoji, GravestoneDoji, LongLeggedDoji, RickshawMan, EveningDojiStar, MorningDojiStar, GapSideBySideWhite, HighWave, Hikkake, HikkakeModified, HomingPigeon, OnNeck, InNeck, Thrusting, SeparatingLines, Kicking, KickingByLength, LadderBottom, MatHold, MatchingLow, LongLine, ShortLine, RisingThreeMethods, FallingThreeMethods, UpsideGapThreeMethods, DownsideGapThreeMethods, StalledPattern, StickSandwich, Takuri, ClosingMarubozu, OpeningMarubozu, TasukiGap, UniqueThreeRiver, ConcealingBabySwallow, OrderBookImbalanceTop1, OrderBookImbalanceFull, Microprice, QuotedSpread, DepthSlope, OrderBookImbalanceTopN, SignedVolume, CumulativeVolumeDelta, TradeImbalance, EffectiveSpread, RealizedSpread, KylesLambda, Footprint, FundingRate, FundingRateMean, FundingRateZScore, FundingBasis, OpenInterestDelta, OIPriceDivergence, OIWeighted, LongShortRatio, TakerBuySellRatio, LiquidationFeatures, TermStructureBasis, CalendarSpread, SharpeRatio, SortinoRatio, CalmarRatio, OmegaRatio, MaxDrawdown, AverageDrawdown, DrawdownDuration, PainIndex, ValueAtRisk, ConditionalValueAtRisk, ProfitFactor, GainLossRatio, RecoveryFactor, KellyCriterion, TreynorRatio, InformationRatio, Alpha } = nativeBinding
|
const { version, SMA, EMA, WMA, RSI, DEMA, TEMA, HMA, ROC, TRIX, SMMA, TRIMA, ZLEMA, MOM, CMO, DPO, StdDev, UlcerIndex, VerticalHorizontalFilter, ZScore, McGinleyDynamic, FRAMA, SuperSmoother, FisherTransform, Decycler, CenterOfGravity, CyberneticCycle, InstantaneousTrendline, EhlersStochastic, RVIVolatility, Variance, CoefficientOfVariation, Skewness, Kurtosis, StandardError, DetrendedStdDev, RSquared, MedianAbsoluteDeviation, Autocorrelation, HurstExponent, PearsonCorrelation, Beta, PairwiseBeta, SpearmanCorrelation, PairSpreadZScore, LeadLagCrossCorrelation, Cointegration, RelativeStrengthAB, MACD, BollingerBands, ATR, Stochastic, OBV, ADX, ADXR, CCI, WilliamsR, MFI, PSAR, Keltner, Donchian, VWAP, RollingVWAP, AwesomeOscillator, Aroon, Inertia, ConnorsRSI, LaguerreRSI, SMI, KST, PGO, RVI, AwesomeOscillatorHistogram, STC, ElderImpulse, ZeroLagMACD, CFO, APO, KAMA, EVWMA, Alligator, JMA, VIDYA, ALMA, T3, TSI, PMO, TII, ADL, VolumePriceTrend, ChaikinMoneyFlow, ChaikinOscillator, ForceIndex, NVI, PVI, VolumeOscillator, KVO, WilliamsAD, AnchoredRSI, AnchoredVWAP, DemandIndex, TSV, VZO, MarketFacilitationIndex, EaseOfMovement, SuperTrend, ChandelierExit, ChandeKrollStop, AtrTrailingStop, HiLoActivator, VoltyStop, YoyoExit, DonchianStop, PercentageTrailingStop, StepTrailingStop, RenkoTrailingStop, TypicalPrice, MedianPrice, WeightedClose, LinearRegression, LinRegSlope, AcceleratorOscillator, BalanceOfPower, ChoppinessIndex, TrueRange, ChaikinVolatility, YangZhangVolatility, RogersSatchellVolatility, GarmanKlassVolatility, ParkinsonVolatility, LinRegAngle, BollingerBandwidth, PercentB, NATR, HistoricalVolatility, AroonOscillator, WaveTrend, RWI, Vortex, MassIndex, StochRSI, UltimateOscillator, PPO, Coppock, VWMA, MaEnvelope, AccelerationBands, StarcBands, AtrBands, HurstChannel, LinRegChannel, StandardErrorBands, DoubleBollinger, TtmSqueeze, FractalChaosBands, VwapStdDevBands, ClassicPivots, FibonacciPivots, Camarilla, WoodiePivots, DemarkPivots, WilliamsFractals, ZigZag, TDSetup, TDSequential, TDDeMarker, TDREI, TDPressure, TDCombo, TDCountdown, TDLines, TDRangeProjection, TDDifferential, TDOpen, TDRiskLevel, InverseFisherTransform, DecyclerOscillator, RoofingFilter, EmpiricalModeDecomposition, HilbertDominantCycle, AdaptiveCycle, SineWave, MAMA, FAMA, Ichimoku, HeikinAshi, ValueArea, InitialBalance, OpeningRange, Doji, Hammer, InvertedHammer, HangingMan, ShootingStar, Engulfing, Harami, MorningEveningStar, ThreeSoldiersOrCrows, PiercingDarkCloud, Marubozu, Tweezer, SpinningTop, ThreeInside, ThreeOutside, TwoCrows, UpsideGapTwoCrows, IdenticalThreeCrows, ThreeLineStrike, ThreeStarsInSouth, AbandonedBaby, AdvanceBlock, BeltHold, Breakaway, Counterattack, DojiStar, DragonflyDoji, GravestoneDoji, LongLeggedDoji, RickshawMan, EveningDojiStar, MorningDojiStar, GapSideBySideWhite, HighWave, Hikkake, HikkakeModified, HomingPigeon, OnNeck, InNeck, Thrusting, SeparatingLines, Kicking, KickingByLength, LadderBottom, MatHold, MatchingLow, LongLine, ShortLine, RisingThreeMethods, FallingThreeMethods, UpsideGapThreeMethods, DownsideGapThreeMethods, StalledPattern, StickSandwich, Takuri, ClosingMarubozu, OpeningMarubozu, TasukiGap, UniqueThreeRiver, ConcealingBabySwallow, OrderBookImbalanceTop1, OrderBookImbalanceFull, Microprice, QuotedSpread, DepthSlope, OrderBookImbalanceTopN, SignedVolume, CumulativeVolumeDelta, TradeImbalance, EffectiveSpread, RealizedSpread, KylesLambda, Footprint, FundingRate, FundingRateMean, FundingRateZScore, FundingBasis, OpenInterestDelta, OIPriceDivergence, OIWeighted, LongShortRatio, TakerBuySellRatio, LiquidationFeatures, TermStructureBasis, CalendarSpread, SharpeRatio, SortinoRatio, CalmarRatio, OmegaRatio, MaxDrawdown, AverageDrawdown, DrawdownDuration, PainIndex, ValueAtRisk, ConditionalValueAtRisk, ProfitFactor, GainLossRatio, RecoveryFactor, KellyCriterion, TreynorRatio, InformationRatio, Alpha } = nativeBinding
|
||||||
|
|
||||||
module.exports.version = version
|
module.exports.version = version
|
||||||
module.exports.SMA = SMA
|
module.exports.SMA = SMA
|
||||||
@@ -410,6 +410,7 @@ module.exports.PVI = PVI
|
|||||||
module.exports.VolumeOscillator = VolumeOscillator
|
module.exports.VolumeOscillator = VolumeOscillator
|
||||||
module.exports.KVO = KVO
|
module.exports.KVO = KVO
|
||||||
module.exports.WilliamsAD = WilliamsAD
|
module.exports.WilliamsAD = WilliamsAD
|
||||||
|
module.exports.AnchoredRSI = AnchoredRSI
|
||||||
module.exports.AnchoredVWAP = AnchoredVWAP
|
module.exports.AnchoredVWAP = AnchoredVWAP
|
||||||
module.exports.DemandIndex = DemandIndex
|
module.exports.DemandIndex = DemandIndex
|
||||||
module.exports.TSV = TSV
|
module.exports.TSV = TSV
|
||||||
|
|||||||
@@ -3179,6 +3179,48 @@ impl AdOscillatorNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================== Anchored RSI ==============================
|
||||||
|
|
||||||
|
#[napi(js_name = "AnchoredRSI")]
|
||||||
|
pub struct AnchoredRsiNode {
|
||||||
|
inner: wc::AnchoredRsi,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
impl AnchoredRsiNode {
|
||||||
|
#[napi(constructor)]
|
||||||
|
#[allow(clippy::new_without_default)]
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
inner: wc::AnchoredRsi::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[napi(js_name = "setAnchor")]
|
||||||
|
pub fn set_anchor(&mut self) {
|
||||||
|
self.inner.set_anchor();
|
||||||
|
}
|
||||||
|
#[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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ============================== Anchored VWAP ==============================
|
// ============================== Anchored VWAP ==============================
|
||||||
|
|
||||||
#[napi(js_name = "AnchoredVWAP")]
|
#[napi(js_name = "AnchoredVWAP")]
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ from ._wickra import (
|
|||||||
EVWMA,
|
EVWMA,
|
||||||
# Momentum
|
# Momentum
|
||||||
RSI,
|
RSI,
|
||||||
|
AnchoredRSI,
|
||||||
MACD,
|
MACD,
|
||||||
Stochastic,
|
Stochastic,
|
||||||
CCI,
|
CCI,
|
||||||
@@ -359,6 +360,7 @@ __all__ = [
|
|||||||
"EVWMA",
|
"EVWMA",
|
||||||
# Momentum
|
# Momentum
|
||||||
"RSI",
|
"RSI",
|
||||||
|
"AnchoredRSI",
|
||||||
"MACD",
|
"MACD",
|
||||||
"Stochastic",
|
"Stochastic",
|
||||||
"CCI",
|
"CCI",
|
||||||
|
|||||||
@@ -5052,6 +5052,58 @@ impl PyAdOscillator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================== Anchored RSI ==============================
|
||||||
|
|
||||||
|
#[pyclass(name = "AnchoredRSI", module = "wickra._wickra", skip_from_py_object)]
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct PyAnchoredRsi {
|
||||||
|
inner: wc::AnchoredRsi,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pymethods]
|
||||||
|
impl PyAnchoredRsi {
|
||||||
|
#[new]
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
inner: wc::AnchoredRsi::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn update(&mut self, value: f64) -> Option<f64> {
|
||||||
|
self.inner.update(value)
|
||||||
|
}
|
||||||
|
/// Re-anchor the cumulative window at the next bar that arrives.
|
||||||
|
fn set_anchor(&mut self) {
|
||||||
|
self.inner.set_anchor();
|
||||||
|
}
|
||||||
|
/// Batch over a close-price numpy column.
|
||||||
|
fn batch<'py>(
|
||||||
|
&mut self,
|
||||||
|
py: Python<'py>,
|
||||||
|
prices: PyReadonlyArray1<'py, f64>,
|
||||||
|
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||||
|
let slice = prices
|
||||||
|
.as_slice()
|
||||||
|
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||||
|
Ok(flatten(self.inner.batch(slice)).into_pyarray(py))
|
||||||
|
}
|
||||||
|
#[getter]
|
||||||
|
fn value(&self) -> Option<f64> {
|
||||||
|
self.inner.value()
|
||||||
|
}
|
||||||
|
fn reset(&mut self) {
|
||||||
|
self.inner.reset();
|
||||||
|
}
|
||||||
|
fn is_ready(&self) -> bool {
|
||||||
|
self.inner.is_ready()
|
||||||
|
}
|
||||||
|
fn warmup_period(&self) -> usize {
|
||||||
|
self.inner.warmup_period()
|
||||||
|
}
|
||||||
|
fn __repr__(&self) -> String {
|
||||||
|
"AnchoredRSI()".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ============================== Anchored VWAP ==============================
|
// ============================== Anchored VWAP ==============================
|
||||||
|
|
||||||
#[pyclass(name = "AnchoredVWAP", module = "wickra._wickra", skip_from_py_object)]
|
#[pyclass(name = "AnchoredVWAP", module = "wickra._wickra", skip_from_py_object)]
|
||||||
@@ -14058,6 +14110,7 @@ fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
|
|||||||
m.add_class::<PyNvi>()?;
|
m.add_class::<PyNvi>()?;
|
||||||
m.add_class::<PyPvi>()?;
|
m.add_class::<PyPvi>()?;
|
||||||
m.add_class::<PyAdOscillator>()?;
|
m.add_class::<PyAdOscillator>()?;
|
||||||
|
m.add_class::<PyAnchoredRsi>()?;
|
||||||
m.add_class::<PyAnchoredVwap>()?;
|
m.add_class::<PyAnchoredVwap>()?;
|
||||||
m.add_class::<PyDemandIndex>()?;
|
m.add_class::<PyDemandIndex>()?;
|
||||||
m.add_class::<PyTsv>()?;
|
m.add_class::<PyTsv>()?;
|
||||||
|
|||||||
@@ -66,6 +66,14 @@ def test_rsi_wilder_textbook_first_value():
|
|||||||
assert math.isclose(out[14], 70.464, abs_tol=0.05)
|
assert math.isclose(out[14], 70.464, abs_tol=0.05)
|
||||||
|
|
||||||
|
|
||||||
|
def test_anchored_rsi_cumulative_reference():
|
||||||
|
"""Cumulative anchored RSI: 10 -> 11 (+1) -> 9 (-2) -> 12 (+3)."""
|
||||||
|
out = ta.AnchoredRSI().batch(np.array([10.0, 11.0, 9.0, 12.0]))
|
||||||
|
assert math.isclose(out[1], 100.0, abs_tol=1e-9)
|
||||||
|
assert math.isclose(out[2], 100.0 - 100.0 / 1.5, abs_tol=1e-6)
|
||||||
|
assert math.isclose(out[3], 100.0 - 100.0 / 3.0, abs_tol=1e-6)
|
||||||
|
|
||||||
|
|
||||||
def test_inertia_constant_rvi_passes_through_linreg():
|
def test_inertia_constant_rvi_passes_through_linreg():
|
||||||
# Every bar identical (open, high, low, close) = (10, 11, 9, 10.5):
|
# Every bar identical (open, high, low, close) = (10, 11, 9, 10.5):
|
||||||
# RVI = (c-o) / (h-l) = 0.5 / 2 = 0.25 every bar. LinReg of a constant
|
# RVI = (c-o) / (h-l) = 0.5 / 2 = 0.25 every bar. LinReg of a constant
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ SCALAR_INDICATORS = [
|
|||||||
(ta.EMA, (14,)),
|
(ta.EMA, (14,)),
|
||||||
(ta.WMA, (14,)),
|
(ta.WMA, (14,)),
|
||||||
(ta.RSI, (14,)),
|
(ta.RSI, (14,)),
|
||||||
|
(ta.AnchoredRSI, ()),
|
||||||
(ta.MACD, ()),
|
(ta.MACD, ()),
|
||||||
(ta.BollingerBands, ()),
|
(ta.BollingerBands, ()),
|
||||||
]
|
]
|
||||||
@@ -42,6 +43,7 @@ def test_reset_returns_to_initial_state(cls, args):
|
|||||||
(ta.EMA, (14,), 14),
|
(ta.EMA, (14,), 14),
|
||||||
(ta.WMA, (14,), 14),
|
(ta.WMA, (14,), 14),
|
||||||
(ta.RSI, (14,), 15),
|
(ta.RSI, (14,), 15),
|
||||||
|
(ta.AnchoredRSI, (), 2),
|
||||||
(ta.BollingerBands, (20, 2.0), 20),
|
(ta.BollingerBands, (20, 2.0), 20),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1193,6 +1193,42 @@ def test_anchored_vwap_set_anchor_clears_window():
|
|||||||
assert v == pytest.approx(100.0)
|
assert v == pytest.approx(100.0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_anchored_rsi_reference():
|
||||||
|
# prices 10 -> 11 (+1) -> 9 (-2) -> 12 (+3); cumulative anchored RSI.
|
||||||
|
# bar2: sum_gain=1, sum_loss=2 -> rs=0.5 -> 100 - 100/1.5 = 33.3333
|
||||||
|
# bar3: sum_gain=4, sum_loss=2 -> rs=2.0 -> 100 - 100/3 = 66.6667
|
||||||
|
rsi = ta.AnchoredRSI()
|
||||||
|
out = rsi.batch(np.array([10.0, 11.0, 9.0, 12.0]))
|
||||||
|
assert np.isnan(out[0])
|
||||||
|
assert out[1] == pytest.approx(100.0)
|
||||||
|
assert out[2] == pytest.approx(33.333333, abs=1e-4)
|
||||||
|
assert out[3] == pytest.approx(66.666666, abs=1e-4)
|
||||||
|
|
||||||
|
|
||||||
|
def test_anchored_rsi_set_anchor_clears_window():
|
||||||
|
# Downtrend reads 0; after re-anchor an uptrend must read a fresh 100.
|
||||||
|
rsi = ta.AnchoredRSI()
|
||||||
|
for p in (20.0, 19.0, 18.0, 17.0):
|
||||||
|
rsi.update(p)
|
||||||
|
assert rsi.is_ready()
|
||||||
|
assert rsi.value == pytest.approx(0.0)
|
||||||
|
rsi.set_anchor()
|
||||||
|
assert rsi.update(50.0) is None
|
||||||
|
assert rsi.update(51.0) == pytest.approx(100.0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_anchored_rsi_streaming_matches_batch():
|
||||||
|
prices = np.array([100.0 + np.sin(i * 0.4) * 8.0 for i in range(60)])
|
||||||
|
batched = ta.AnchoredRSI().batch(prices)
|
||||||
|
streamer = ta.AnchoredRSI()
|
||||||
|
streamed = [streamer.update(float(p)) for p in prices]
|
||||||
|
for b, s in zip(batched, streamed):
|
||||||
|
if np.isnan(b):
|
||||||
|
assert s is None
|
||||||
|
else:
|
||||||
|
assert s == pytest.approx(b)
|
||||||
|
|
||||||
|
|
||||||
def test_tsv_reference():
|
def test_tsv_reference():
|
||||||
# closes = [10, 11, 13, 12, 14, 15]
|
# closes = [10, 11, 13, 12, 14, 15]
|
||||||
# volumes = [50, 100, 200, 150, 50, 200]
|
# volumes = [50, 100, 200, 150, 50, 200]
|
||||||
|
|||||||
@@ -1682,6 +1682,44 @@ impl WasmAdOscillator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = AnchoredRSI)]
|
||||||
|
pub struct WasmAnchoredRsi {
|
||||||
|
inner: wc::AnchoredRsi,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_class = AnchoredRSI)]
|
||||||
|
impl WasmAnchoredRsi {
|
||||||
|
#[wasm_bindgen(constructor)]
|
||||||
|
#[allow(clippy::new_without_default)]
|
||||||
|
pub fn new() -> WasmAnchoredRsi {
|
||||||
|
Self {
|
||||||
|
inner: wc::AnchoredRsi::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[wasm_bindgen(js_name = setAnchor)]
|
||||||
|
pub fn set_anchor(&mut self) {
|
||||||
|
self.inner.set_anchor();
|
||||||
|
}
|
||||||
|
pub fn update(&mut self, value: f64) -> Option<f64> {
|
||||||
|
self.inner.update(value)
|
||||||
|
}
|
||||||
|
pub fn batch(&mut self, prices: &[f64]) -> Float64Array {
|
||||||
|
let out = flatten(self.inner.batch(prices));
|
||||||
|
Float64Array::from(out.as_slice())
|
||||||
|
}
|
||||||
|
pub fn reset(&mut self) {
|
||||||
|
self.inner.reset();
|
||||||
|
}
|
||||||
|
#[wasm_bindgen(js_name = isReady)]
|
||||||
|
pub fn is_ready(&self) -> bool {
|
||||||
|
self.inner.is_ready()
|
||||||
|
}
|
||||||
|
#[wasm_bindgen(js_name = warmupPeriod)]
|
||||||
|
pub fn warmup_period(&self) -> usize {
|
||||||
|
self.inner.warmup_period()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = AnchoredVWAP)]
|
#[wasm_bindgen(js_name = AnchoredVWAP)]
|
||||||
pub struct WasmAnchoredVwap {
|
pub struct WasmAnchoredVwap {
|
||||||
inner: wc::AnchoredVwap,
|
inner: wc::AnchoredVwap,
|
||||||
|
|||||||
@@ -0,0 +1,284 @@
|
|||||||
|
//! Anchored Relative Strength Index.
|
||||||
|
|
||||||
|
use crate::traits::Indicator;
|
||||||
|
|
||||||
|
/// Anchored RSI — a cumulative Relative Strength Index whose averaging begins at
|
||||||
|
/// a user-chosen anchor bar rather than over a fixed Wilder period.
|
||||||
|
///
|
||||||
|
/// Where [`crate::Rsi`] uses Wilder's `period`-length smoothing, Anchored RSI
|
||||||
|
/// accumulates *every* up- and down-move since the anchor with equal weight, so
|
||||||
|
/// it answers "what is the RSI of the entire move since the anchor point?". The
|
||||||
|
/// running relative strength is `Σ gains / Σ losses` over all bars in the
|
||||||
|
/// current anchor window (the bar count cancels, so this equals
|
||||||
|
/// `avg_gain / avg_loss`):
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// RSI_t = 100 - 100 / (1 + Σ_{i ≥ anchor} gain_i / Σ_{i ≥ anchor} loss_i)
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// As with [`crate::AnchoredVwap`], the anchor is chosen at runtime:
|
||||||
|
/// [`AnchoredRsi::set_anchor`] re-anchors at the **next** bar that arrives,
|
||||||
|
/// clearing the running sums. Because RSI needs a price *change*, the first bar
|
||||||
|
/// of a fresh anchor window only seeds the previous close and emits `None`; the
|
||||||
|
/// first value follows on the second bar (warmup period 2).
|
||||||
|
///
|
||||||
|
/// Saturation follows the standard convention: a window with no losses yet (and
|
||||||
|
/// at least one gain) reads 100, no gains yet reads 0, and a perfectly flat
|
||||||
|
/// window reads the neutral 50. Non-finite inputs are ignored, leaving the last
|
||||||
|
/// value unchanged.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use wickra_core::{AnchoredRsi, Indicator};
|
||||||
|
///
|
||||||
|
/// let mut indicator = AnchoredRsi::new();
|
||||||
|
/// let mut last = None;
|
||||||
|
/// for i in 0..80 {
|
||||||
|
/// let price = 100.0 + (f64::from(i) * 0.5).sin() * 5.0;
|
||||||
|
/// // Re-anchor at bar 40 (e.g. a major swing low).
|
||||||
|
/// if i == 40 {
|
||||||
|
/// indicator.set_anchor();
|
||||||
|
/// }
|
||||||
|
/// last = indicator.update(price);
|
||||||
|
/// }
|
||||||
|
/// assert!(last.is_some());
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct AnchoredRsi {
|
||||||
|
prev_close: Option<f64>,
|
||||||
|
sum_gain: f64,
|
||||||
|
sum_loss: f64,
|
||||||
|
last_value: Option<f64>,
|
||||||
|
pending_anchor: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AnchoredRsi {
|
||||||
|
/// Construct a fresh Anchored RSI. The first bar to arrive is the anchor.
|
||||||
|
pub const fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
prev_close: None,
|
||||||
|
sum_gain: 0.0,
|
||||||
|
sum_loss: 0.0,
|
||||||
|
last_value: None,
|
||||||
|
pending_anchor: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mark a re-anchor: the **next** [`Indicator::update`] call clears the
|
||||||
|
/// running sums and previous close before folding in its own bar, starting
|
||||||
|
/// a fresh anchored window.
|
||||||
|
pub fn set_anchor(&mut self) {
|
||||||
|
self.pending_anchor = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Current anchored RSI value if at least one price change has been
|
||||||
|
/// observed in the current anchor window.
|
||||||
|
pub const fn value(&self) -> Option<f64> {
|
||||||
|
self.last_value
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rsi_from_sums(sum_gain: f64, sum_loss: f64) -> f64 {
|
||||||
|
if sum_loss == 0.0 {
|
||||||
|
if sum_gain == 0.0 {
|
||||||
|
// No movement at all -> RSI undefined; standard convention returns 50.
|
||||||
|
50.0
|
||||||
|
} else {
|
||||||
|
100.0
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let rs = sum_gain / sum_loss;
|
||||||
|
100.0 - 100.0 / (1.0 + rs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Indicator for AnchoredRsi {
|
||||||
|
type Input = f64;
|
||||||
|
type Output = f64;
|
||||||
|
|
||||||
|
fn update(&mut self, input: f64) -> Option<f64> {
|
||||||
|
if !input.is_finite() {
|
||||||
|
return self.last_value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.pending_anchor {
|
||||||
|
self.prev_close = None;
|
||||||
|
self.sum_gain = 0.0;
|
||||||
|
self.sum_loss = 0.0;
|
||||||
|
self.last_value = None;
|
||||||
|
self.pending_anchor = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(prev) = self.prev_close else {
|
||||||
|
self.prev_close = Some(input);
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
self.prev_close = Some(input);
|
||||||
|
|
||||||
|
let diff = input - prev;
|
||||||
|
if diff > 0.0 {
|
||||||
|
self.sum_gain += diff;
|
||||||
|
} else if diff < 0.0 {
|
||||||
|
self.sum_loss -= diff;
|
||||||
|
}
|
||||||
|
|
||||||
|
let value = Self::rsi_from_sums(self.sum_gain, self.sum_loss);
|
||||||
|
self.last_value = Some(value);
|
||||||
|
Some(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(&mut self) {
|
||||||
|
self.prev_close = None;
|
||||||
|
self.sum_gain = 0.0;
|
||||||
|
self.sum_loss = 0.0;
|
||||||
|
self.last_value = None;
|
||||||
|
self.pending_anchor = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn warmup_period(&self) -> usize {
|
||||||
|
2
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_ready(&self) -> bool {
|
||||||
|
self.last_value.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"AnchoredRSI"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::traits::BatchExt;
|
||||||
|
use approx::assert_relative_eq;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn accessors_and_metadata() {
|
||||||
|
let indicator = AnchoredRsi::new();
|
||||||
|
assert_eq!(indicator.name(), "AnchoredRSI");
|
||||||
|
assert_eq!(indicator.warmup_period(), 2);
|
||||||
|
assert_eq!(indicator.value(), None);
|
||||||
|
assert!(!indicator.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn first_bar_seeds_and_returns_none() {
|
||||||
|
let mut indicator = AnchoredRsi::new();
|
||||||
|
assert_eq!(indicator.update(100.0), None);
|
||||||
|
assert!(!indicator.is_ready());
|
||||||
|
// Second bar produces the first value.
|
||||||
|
assert!(indicator.update(101.0).is_some());
|
||||||
|
assert!(indicator.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pure_uptrend_saturates_at_100() {
|
||||||
|
let mut indicator = AnchoredRsi::new();
|
||||||
|
let out = indicator.batch(&[10.0, 11.0, 12.0, 13.0]);
|
||||||
|
assert_relative_eq!(out[3].unwrap(), 100.0, epsilon = 1e-12);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pure_downtrend_saturates_at_0() {
|
||||||
|
let mut indicator = AnchoredRsi::new();
|
||||||
|
let out = indicator.batch(&[13.0, 12.0, 11.0, 10.0]);
|
||||||
|
assert_relative_eq!(out[3].unwrap(), 0.0, epsilon = 1e-12);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn flat_window_reads_50() {
|
||||||
|
let mut indicator = AnchoredRsi::new();
|
||||||
|
let out = indicator.batch(&[42.0, 42.0, 42.0]);
|
||||||
|
assert_relative_eq!(out[2].unwrap(), 50.0, epsilon = 1e-12);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cumulative_reference_values() {
|
||||||
|
// prices 10 -> 11 (+1) -> 9 (-2) -> 12 (+3)
|
||||||
|
// after bar2: sum_gain=1, sum_loss=2 -> rs=0.5 -> 100 - 100/1.5 = 33.3333
|
||||||
|
// after bar3: sum_gain=4, sum_loss=2 -> rs=2.0 -> 100 - 100/3 = 66.6667
|
||||||
|
let mut indicator = AnchoredRsi::new();
|
||||||
|
let out = indicator.batch(&[10.0, 11.0, 9.0, 12.0]);
|
||||||
|
assert_relative_eq!(out[1].unwrap(), 100.0, epsilon = 1e-9);
|
||||||
|
assert_relative_eq!(out[2].unwrap(), 33.333_333_333, epsilon = 1e-6);
|
||||||
|
assert_relative_eq!(out[3].unwrap(), 66.666_666_666, epsilon = 1e-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_anchor_clears_old_window() {
|
||||||
|
// Downtrend, then re-anchor and pump an uptrend: the new window must
|
||||||
|
// read 100, not the blended value.
|
||||||
|
let mut indicator = AnchoredRsi::new();
|
||||||
|
indicator.batch(&[20.0, 19.0, 18.0, 17.0]);
|
||||||
|
assert_relative_eq!(indicator.value().unwrap(), 0.0, epsilon = 1e-12);
|
||||||
|
indicator.set_anchor();
|
||||||
|
// First bar after anchor re-seeds (None), second bar emits.
|
||||||
|
assert_eq!(indicator.update(50.0), None);
|
||||||
|
let after = indicator.update(51.0).unwrap();
|
||||||
|
assert_relative_eq!(after, 100.0, epsilon = 1e-12);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_anchor_before_first_bar_acts_as_normal_start() {
|
||||||
|
let mut indicator = AnchoredRsi::new();
|
||||||
|
indicator.set_anchor();
|
||||||
|
assert_eq!(indicator.update(10.0), None);
|
||||||
|
assert_relative_eq!(indicator.update(11.0).unwrap(), 100.0, epsilon = 1e-12);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ignores_non_finite_input() {
|
||||||
|
let mut indicator = AnchoredRsi::new();
|
||||||
|
indicator.batch(&[10.0, 11.0, 12.0]);
|
||||||
|
let before = indicator.value();
|
||||||
|
assert!(before.is_some());
|
||||||
|
assert_eq!(indicator.update(f64::NAN), before);
|
||||||
|
assert_eq!(indicator.update(f64::INFINITY), before);
|
||||||
|
assert_eq!(indicator.value(), before);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn non_finite_before_any_bar_returns_none() {
|
||||||
|
let mut indicator = AnchoredRsi::new();
|
||||||
|
assert_eq!(indicator.update(f64::NAN), None);
|
||||||
|
assert!(!indicator.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reset_clears_state() {
|
||||||
|
let mut indicator = AnchoredRsi::new();
|
||||||
|
indicator.batch(&[10.0, 11.0, 12.0]);
|
||||||
|
assert!(indicator.is_ready());
|
||||||
|
indicator.reset();
|
||||||
|
assert!(!indicator.is_ready());
|
||||||
|
assert_eq!(indicator.value(), None);
|
||||||
|
assert_eq!(indicator.update(50.0), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn stays_in_0_100_range() {
|
||||||
|
let prices: Vec<f64> = (0..200)
|
||||||
|
.map(|i| 100.0 + (f64::from(i) * 0.7).sin() * 10.0)
|
||||||
|
.collect();
|
||||||
|
let mut indicator = AnchoredRsi::new();
|
||||||
|
for value in indicator.batch(&prices).into_iter().flatten() {
|
||||||
|
assert!((0.0..=100.0).contains(&value), "RSI out of range: {value}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn batch_equals_streaming() {
|
||||||
|
let prices: Vec<f64> = (1..=40)
|
||||||
|
.map(|i| (f64::from(i) * 0.3).sin() * 5.0 + f64::from(i))
|
||||||
|
.collect();
|
||||||
|
let mut a = AnchoredRsi::new();
|
||||||
|
let mut b = AnchoredRsi::new();
|
||||||
|
assert_eq!(
|
||||||
|
a.batch(&prices),
|
||||||
|
prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ mod adxr;
|
|||||||
mod alligator;
|
mod alligator;
|
||||||
mod alma;
|
mod alma;
|
||||||
mod alpha;
|
mod alpha;
|
||||||
|
mod anchored_rsi;
|
||||||
mod anchored_vwap;
|
mod anchored_vwap;
|
||||||
mod apo;
|
mod apo;
|
||||||
mod aroon;
|
mod aroon;
|
||||||
@@ -305,6 +306,7 @@ pub use adxr::Adxr;
|
|||||||
pub use alligator::{Alligator, AlligatorOutput};
|
pub use alligator::{Alligator, AlligatorOutput};
|
||||||
pub use alma::Alma;
|
pub use alma::Alma;
|
||||||
pub use alpha::Alpha;
|
pub use alpha::Alpha;
|
||||||
|
pub use anchored_rsi::AnchoredRsi;
|
||||||
pub use anchored_vwap::AnchoredVwap;
|
pub use anchored_vwap::AnchoredVwap;
|
||||||
pub use apo::Apo;
|
pub use apo::Apo;
|
||||||
pub use aroon::{Aroon, AroonOutput};
|
pub use aroon::{Aroon, AroonOutput};
|
||||||
@@ -617,6 +619,7 @@ pub const FAMILIES: &[(&str, &[&str])] = &[
|
|||||||
"Momentum Oscillators",
|
"Momentum Oscillators",
|
||||||
&[
|
&[
|
||||||
"Rsi",
|
"Rsi",
|
||||||
|
"AnchoredRsi",
|
||||||
"Stochastic",
|
"Stochastic",
|
||||||
"Cci",
|
"Cci",
|
||||||
"Roc",
|
"Roc",
|
||||||
@@ -981,6 +984,6 @@ mod family_tests {
|
|||||||
// the actual indicator count is the early-warning signal that an
|
// the actual indicator count is the early-warning signal that an
|
||||||
// indicator was added without being assigned a family.
|
// indicator was added without being assigned a family.
|
||||||
let total: usize = FAMILIES.iter().map(|(_, ns)| ns.len()).sum();
|
let total: usize = FAMILIES.iter().map(|(_, ns)| ns.len()).sum();
|
||||||
assert_eq!(total, 284, "FAMILIES total drifted from indicator count");
|
assert_eq!(total, 285, "FAMILIES total drifted from indicator count");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,8 +55,8 @@ pub use error::{Error, Result};
|
|||||||
pub use indicators::{
|
pub use indicators::{
|
||||||
AbandonedBaby, AccelerationBands, AccelerationBandsOutput, AcceleratorOscillator, AdOscillator,
|
AbandonedBaby, AccelerationBands, AccelerationBandsOutput, AcceleratorOscillator, AdOscillator,
|
||||||
AdaptiveCycle, Adl, AdvanceBlock, Adx, AdxOutput, Adxr, Alligator, AlligatorOutput, Alma,
|
AdaptiveCycle, Adl, AdvanceBlock, Adx, AdxOutput, Adxr, Alligator, AlligatorOutput, Alma,
|
||||||
Alpha, AnchoredVwap, Apo, Aroon, AroonOscillator, AroonOutput, Atr, AtrBands, AtrBandsOutput,
|
Alpha, AnchoredRsi, AnchoredVwap, Apo, Aroon, AroonOscillator, AroonOutput, Atr, AtrBands,
|
||||||
AtrTrailingStop, Autocorrelation, AverageDrawdown, AwesomeOscillator,
|
AtrBandsOutput, AtrTrailingStop, Autocorrelation, AverageDrawdown, AwesomeOscillator,
|
||||||
AwesomeOscillatorHistogram, BalanceOfPower, BeltHold, Beta, BollingerBands, BollingerBandwidth,
|
AwesomeOscillatorHistogram, BalanceOfPower, BeltHold, Beta, BollingerBands, BollingerBandwidth,
|
||||||
BollingerOutput, Breakaway, CalendarSpread, CalmarRatio, Camarilla, CamarillaPivotsOutput, Cci,
|
BollingerOutput, Breakaway, CalendarSpread, CalmarRatio, Camarilla, CamarillaPivotsOutput, Cci,
|
||||||
CenterOfGravity, Cfo, ChaikinMoneyFlow, ChaikinOscillator, ChaikinVolatility, ChandeKrollStop,
|
CenterOfGravity, Cfo, ChaikinMoneyFlow, ChaikinOscillator, ChaikinVolatility, ChandeKrollStop,
|
||||||
|
|||||||
@@ -32,8 +32,8 @@
|
|||||||
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
|
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
|
||||||
use std::hint::black_box;
|
use std::hint::black_box;
|
||||||
use wickra::{
|
use wickra::{
|
||||||
Adx, Atr, Autocorrelation, BatchExt, BollingerBands, BollingerOutput, CalmarRatio, Candle, Cci,
|
Adx, AnchoredRsi, Atr, Autocorrelation, BatchExt, BollingerBands, BollingerOutput, CalmarRatio,
|
||||||
ClassicPivots, ConnorsRsi, DepthSlope, DerivativesTick, EffectiveSpread, Ema,
|
Candle, Cci, ClassicPivots, ConnorsRsi, DepthSlope, DerivativesTick, EffectiveSpread, Ema,
|
||||||
EmpiricalModeDecomposition, Engulfing, Frama, FundingRate, FundingRateZScore,
|
EmpiricalModeDecomposition, Engulfing, Frama, FundingRate, FundingRateZScore,
|
||||||
HilbertDominantCycle, HurstExponent, Ichimoku, IchimokuOutput, Indicator, Jma, KylesLambda,
|
HilbertDominantCycle, HurstExponent, Ichimoku, IchimokuOutput, Indicator, Jma, KylesLambda,
|
||||||
Level, LinearRegression, MacdIndicator, MacdOutput, Mama, MamaOutput, MaxDrawdown, Microprice,
|
Level, LinearRegression, MacdIndicator, MacdOutput, Mama, MamaOutput, MaxDrawdown, Microprice,
|
||||||
@@ -246,6 +246,7 @@ fn benches(c: &mut Criterion) {
|
|||||||
// === Family 02 — Momentum Oscillators ===
|
// === Family 02 — Momentum Oscillators ===
|
||||||
// Rsi: textbook baseline; ConnorsRsi: three-component composite.
|
// Rsi: textbook baseline; ConnorsRsi: three-component composite.
|
||||||
bench_scalar(c, "rsi", &closes, || Rsi::new(14).unwrap());
|
bench_scalar(c, "rsi", &closes, || Rsi::new(14).unwrap());
|
||||||
|
bench_scalar(c, "anchored_rsi", &closes, AnchoredRsi::new);
|
||||||
bench_candle_input(c, "cci", &candles, || Cci::new(20).unwrap());
|
bench_candle_input(c, "cci", &candles, || Cci::new(20).unwrap());
|
||||||
bench_scalar(c, "connors_rsi", &closes, ConnorsRsi::classic);
|
bench_scalar(c, "connors_rsi", &closes, ConnorsRsi::classic);
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -8,7 +8,7 @@ That includes:
|
|||||||
[Python](https://docs.wickra.org/Quickstart-Python),
|
[Python](https://docs.wickra.org/Quickstart-Python),
|
||||||
[Node](https://docs.wickra.org/Quickstart-Node), and
|
[Node](https://docs.wickra.org/Quickstart-Node), and
|
||||||
[WASM](https://docs.wickra.org/Quickstart-WASM).
|
[WASM](https://docs.wickra.org/Quickstart-WASM).
|
||||||
- A per-indicator deep dive for every one of the **289 indicators** across
|
- A per-indicator deep dive for every one of the **290 indicators** across
|
||||||
the sixteen families (Moving Averages, Momentum Oscillators, Trend &
|
the sixteen families (Moving Averages, Momentum Oscillators, Trend &
|
||||||
Directional, Price Oscillators, Volatility & Bands, Bands & Channels,
|
Directional, Price Oscillators, Volatility & Bands, Bands & Channels,
|
||||||
Trailing Stops, Volume, Price Statistics, Ehlers / Cycle DSP, Pivots &
|
Trailing Stops, Volume, Price Statistics, Ehlers / Cycle DSP, Pivots &
|
||||||
|
|||||||
@@ -15,7 +15,8 @@
|
|||||||
|
|
||||||
use libfuzzer_sys::fuzz_target;
|
use libfuzzer_sys::fuzz_target;
|
||||||
use wickra_core::{
|
use wickra_core::{
|
||||||
AdaptiveCycle, Alma, Apo, Autocorrelation, AverageDrawdown, BatchExt, Beta, BollingerBands,
|
AdaptiveCycle, Alma, AnchoredRsi, Apo, Autocorrelation, AverageDrawdown, BatchExt, Beta,
|
||||||
|
BollingerBands,
|
||||||
CalmarRatio, CenterOfGravity, Cfo, Cmo, CoefficientOfVariation, ConditionalValueAtRisk,
|
CalmarRatio, CenterOfGravity, Cfo, Cmo, CoefficientOfVariation, ConditionalValueAtRisk,
|
||||||
ConnorsRsi, Coppock, CyberneticCycle, Decycler, DecyclerOscillator, Dema, DetrendedStdDev,
|
ConnorsRsi, Coppock, CyberneticCycle, Decycler, DecyclerOscillator, Dema, DetrendedStdDev,
|
||||||
DoubleBollinger, Dpo, DrawdownDuration, EhlersStochastic, ElderImpulse, Ema,
|
DoubleBollinger, Dpo, DrawdownDuration, EhlersStochastic, ElderImpulse, Ema,
|
||||||
@@ -54,6 +55,7 @@ fuzz_target!(|data: Vec<f64>| {
|
|||||||
drive(|| Ema::new(20).unwrap(), &data);
|
drive(|| Ema::new(20).unwrap(), &data);
|
||||||
drive(|| Wma::new(14).unwrap(), &data);
|
drive(|| Wma::new(14).unwrap(), &data);
|
||||||
drive(|| Rsi::new(14).unwrap(), &data);
|
drive(|| Rsi::new(14).unwrap(), &data);
|
||||||
|
drive(AnchoredRsi::new, &data);
|
||||||
drive(|| Dema::new(14).unwrap(), &data);
|
drive(|| Dema::new(14).unwrap(), &data);
|
||||||
drive(|| Tema::new(14).unwrap(), &data);
|
drive(|| Tema::new(14).unwrap(), &data);
|
||||||
drive(|| Hma::new(14).unwrap(), &data);
|
drive(|| Hma::new(14).unwrap(), &data);
|
||||||
|
|||||||
Reference in New Issue
Block a user