The 33 Markdown files under docs/wiki/ were never tracked. Commit them
into the repository so the documentation is versioned alongside the
code: 8 top-level pages plus 25 per-indicator deep dives under
indicators/{momentum,trend,volatility,volume}/.
The pages are kept in-repo (not pushed to a flat GitHub Wiki), so the
relative indicators/<family>/... links in Home.md resolve correctly
when rendered on GitHub.
5.8 KiB
Indicator Chaining
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.
Construction
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.
Worked example: EMA(14) → RSI(7)
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:
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.)
Why first emission is at input 21, and warmup_period is 22
Chain::warmup_period is implemented conservatively:
fn warmup_period(&self) -> usize {
self.first.warmup_period() + self.second.warmup_period()
}
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":
if chain.is_ready() {
if let Some(v) = chain.update(price) {
// ...
}
}
State, reset, and Send
Chain<A, B> propagates reset() to both stages:
fn reset(&mut self) {
self.first.reset();
self.second.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 out: Vec<Option<f64>> = chain.batch(&prices);
Stages from different families
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.
Python and Node
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.
See also
- 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/kingchenc/wickra