Chain<A, B> wires the output of one indicator straight into the input of
another. Both stages must agree on f64 as the bridging type, which is the
case for the vast majority of price-in / value-out indicators. The chain
itself is an Indicator, so chains can be nested arbitrarily and used
anywhere a single indicator is accepted.
This page documents the public API of Chain in
crates/wickra-core/src/traits.rs, the worked EMA(14) → RSI(7) example
that the doctest pins, and the warmup-stacking rule.
use wickra::{Chain, Ema, Rsi};
// Two stages.
let chain = Chain::new(Ema::new(14)?, Rsi::new(7)?);
// Three stages — note the `.then(third)` builder method.
let triple = Chain::new(Ema::new(14)?, Ema::new(5)?).then(Rsi::new(7)?);The type of triple is Chain<Chain<Ema, Ema>, Rsi>. You can keep chaining
indefinitely; the Chain<A, B> produced at each step also implements
Indicator<Input = f64, Output = ...>, which is the constraint
.then(third) needs to satisfy.
Both first() and second() accessors return references to the underlying
stages if you need to inspect them; the chain owns its stages by value.
This is the canonical chain example from the doctest on Chain in
crates/wickra-core/src/traits.rs:
use wickra::{Chain, Ema, Indicator, Rsi};
let mut chain = Chain::new(Ema::new(14)?, Rsi::new(7)?);
for i in 1..=21 {
chain.update(f64::from(i));
}
assert!(chain.is_ready());The semantic shape of the chain is: smooth the input series with an
EMA(14), then compute an RSI(7) over the smoothed series — not the
raw inputs. chain.update(price) is the only thing your caller code ever
sees; the EMA-then-RSI plumbing is internal to the Chain value.
The chain emits its first non-None value at input 21:
use wickra::{Chain, Ema, Indicator, Rsi};
let mut chain = Chain::new(Ema::new(14)?, Rsi::new(7)?);
for i in 1..=22 {
if let Some(v) = chain.update(f64::from(i)) {
println!("chain emitted at input #{i}: {v}");
}
}
println!("chain.warmup_period() = {}", chain.warmup_period());Output:
chain emitted at input #21: 100
chain emitted at input #22: 100
chain.warmup_period() = 22
(The value is 100 because the input is the monotonic ramp 1, 2, ..., 22;
an RSI on a strictly increasing series is 100 by construction. The point
is the timing of the first emission.)
Chain::warmup_period is implemented conservatively:
use wickra::{Chain, Ema, Indicator, Rsi};
// Chain::warmup_period is the sum of both stages' warmups:
let chain = Chain::new(Ema::new(14)?, Rsi::new(7)?);
assert_eq!(chain.warmup_period(), 14 + 8);For Chain::new(Ema::new(14)?, Rsi::new(7)?) this expands to
14 + 8 = 22 (RSI(7)'s warmup is period + 1 = 8 — see
Warmup Periods for the off-by-one detail).
In practice the chain emits one input earlier than that conservative sum
because the moment EMA(14) starts producing values is input 14, and RSI(7)
needs 8 EMA outputs to seed, so RSI(7) is ready on EMA output number 8,
which corresponds to input 14 + 7 = 21. The conservative formula
first.warmup + second.warmup ignores this overlap; treat
warmup_period() as an upper bound and is_ready() as the source of truth
for "can I read a value yet":
use wickra::{Chain, Ema, Indicator, Rsi};
let mut chain = Chain::new(Ema::new(14)?, Rsi::new(7)?);
let price = 100.0;
if chain.is_ready() {
if let Some(v) = chain.update(price) {
// ...
}
}Chain<A, B> propagates reset() to both stages:
use wickra::{Chain, Ema, Indicator, Rsi};
// Chain::reset propagates to both stages:
let mut chain = Chain::new(Ema::new(14)?, Rsi::new(7)?);
chain.reset();So calling chain.reset() returns the whole pipeline to the state of a
freshly constructed Chain::new(A::new(...), B::new(...)). Because both
stages are owned by value and the trait Indicator is auto-derive-friendly,
the chain inherits Clone, Debug, and (where each stage is) Send.
The batch extension comes through BatchExt automatically — there's
nothing chain-specific to call:
use wickra::{BatchExt, Chain, Ema, Rsi};
let mut chain = Chain::new(Ema::new(14)?, Rsi::new(7)?);
let prices: Vec<f64> = Vec::new(); // your price series
let out: Vec<Option<f64>> = chain.batch(&prices);The bridging type is f64, so anything Indicator<Input = f64, Output = f64>
can serve as the first stage and anything Indicator<Input = f64> can serve
as the second (or third, or fourth). Indicators that consume Candle /
Tick (Atr, Adx, Stochastic, Mfi, Vwap, Psar, Keltner,
Donchian, Aroon, AwesomeOscillator, Obv) cannot sit at the second
stage of a chain because their Input is not f64. They can still be used
as standalone indicators alongside a chain — wire them in your own loop.
A multi-output indicator like MacdIndicator or BollingerBands can sit
last in a chain (its Output is a struct, but the chain only requires
Input = f64 on the second stage). For example,
Chain::new(Sma::new(20)?, MacdIndicator::classic()) is a valid type:
MACD-of-smoothed-prices.
John Ehlers' digital-signal-processing indicators are designed to be
composed: a band-pass / smoothing filter cleans the price series, and an
oscillator reads the filtered output. Every one of Wickra's Ehlers scalar
filters — RoofingFilter,
SuperSmoother, Decycler — is
Indicator<Input = f64, Output = f64>, so they slot straight into the first
stage of a chain feeding EhlersStochastic (also
f64 → f64).
Ehlers' canonical pairing is the Roofing Filter into the Ehlers Stochastic — the roofing filter strips both the low-frequency trend and the high-frequency noise so the stochastic sees only the tradeable cycle band:
use wickra::{BatchExt, Chain, EhlersStochastic, Indicator, RoofingFilter};
// RoofingFilter(low-pass 10, high-pass 48) → EhlersStochastic(20).
let mut chain = Chain::new(RoofingFilter::new(10, 48)?, EhlersStochastic::new(20)?);
let prices: Vec<f64> = Vec::new(); // your price series
let out: Vec<Option<f64>> = chain.batch(&prices);A SuperSmoother (a two-pole low-pass filter) makes an equally valid
pre-filter when you only want to remove noise without de-trending:
use wickra::{Chain, EhlersStochastic, SuperSmoother};
// Smooth first, then read the cycle position of the smoothed series.
let smoothed_stoch = Chain::new(SuperSmoother::new(10)?, EhlersStochastic::new(20)?);As with every chain, warmup_period() is the conservative sum of the two
stages' warmups (RoofingFilter::warmup_period() + EhlersStochastic::warmup_period()),
and is_ready() is the source of truth for the first readable value — the
filtered series usually becomes available a little earlier than the
conservative sum (see the EMA→RSI analysis above).
Chain is currently a Rust-only construct; the Python and Node bindings
expose individual indicators only. The straightforward equivalent in those
languages is a manual two-step loop:
import wickra as ta
ema = ta.EMA(14)
rsi = ta.RSI(7)
for price in prices:
smoothed = ema.update(price)
if smoothed is not None:
chained = rsi.update(smoothed)
if chained is not None:
...This is exactly what Chain::update does in Rust, transcribed to the
binding's update method. No information is lost.
- Quickstart: Rust — the
Chainexample in context. - Warmup Periods — the underlying stage formulas, and
the RSI
period + 1off-by-one. - Streaming vs Batch —
Chainworks with both paths automatically, viaBatchExt. - Source: https://github.com/wickra-lib/wickra