Files
wickra/docs/wiki/indicators/trend/Indicator-T3.md
T
kingchenc 780a176072 F2: add ZLEMA, T3 and VWMA advanced moving averages
Completes the F2 family (Advanced MAs) end to end:

- Rust core: zlema.rs (Zero-Lag EMA over the de-lagged series
  2·price − price[lag]), t3.rs (Tillson's six-EMA cascade with the
  volume-factor polynomial), vwma.rs (volume-weighted rolling mean with
  a zero-volume fallback to the unweighted mean). Each with a full
  Indicator impl, runnable doctest and reference-value / warmup /
  reset / batch==streaming / non-finite tests.
- Python: PyZlema / PyT3 / PyVwma PyO3 classes + module registration
  + .pyi stubs (T3 defaults v=0.7).
- Node: ZlemaNode via the scalar macro, explicit T3Node and VwmaNode
  classes; index.d.ts and index.js updated.
- WASM: WasmZlema / WasmT3 via the scalar macro, explicit WasmVwma.
- Wiki: Indicator-Zlema.md, Indicator-T3.md, Indicator-Vwma.md plus
  rows in Indicators-Overview.md and entries in Home.md.

cargo fmt + clippy (core/wickra/data/wasm/node) clean; 232 core tests,
25 data tests and 33 doctests green.
2026-05-22 17:45:02 +02:00

5.5 KiB
Raw Blame History

T3

Tillson T3 — a six-fold cascaded EMA recombined with a volume factor v to give a smooth, low-lag trend line.

Quick reference

Field Value
Family Trend
Sub-category Exponential family
Input type f64 (single close)
Output type f64
Output range unbounded; tracks the input price scale
Default parameters period required; v = 0.7 (Python default)
Warmup period 6·period 5
Interpretation Smooth trend line with less lag than a same-period EMA.

Formula

T3 is the generalised DEMA (GD) applied three times. Tim Tillson's expansion of GD(GD(GD(price))) over six chained EMAs — e1 … e6, each of the same period, where e2 = EMA(e1), e3 = EMA(e2), … — is:

v2 = v²,  v3 = v³
c1 = v3
c2 = 3·v2 + 3·v3
c3 = 6·v2  3·v  3·v3
c4 = 1 + 3·v + v3 + 3·v2
T3 = c1·e6 + c2·e5 + c3·e4 + c4·e3

The four coefficients always sum to 1, so a constant price series maps to itself. The volume factor v controls the lag/overshoot trade-off: v = 0 collapses T3 to the plain triple-cascaded EMA e3; the conventional v = 0.7 adds a corrective hump that sharpens turns.

Parameters

Name Type Default Valid range Description
period usize none >= 1 Length of every EMA in the cascade. period = 0 errors with Error::PeriodZero.
v f64 0.7 (Python) [0.0, 1.0] Volume factor. Non-finite or out-of-range values error with Error::InvalidPeriod.

The Python binding defaults v to 0.7 via #[pyo3(signature = (period, v=0.7))]; period is always explicit. The Node and WASM constructors take both arguments explicitly.

Inputs / Outputs

From crates/wickra-core/src/indicators/t3.rs:

impl Indicator for T3 {
    type Input = f64;
    type Output = f64;
    // update(&mut self, input: f64) -> Option<f64>
}

A single f64 close in, an Option<f64> out. Python maps this to float | None / numpy.ndarray (NaN warmup); Node to number | null / Array<number> (NaN warmup).

Warmup

T3::new(period, v).warmup_period() == 6·period 5. Each stage of the SMA-seeded EMA cascade adds period 1 bars of delay: e1 seeds at input period, e2 at 2·period 1, …, e6 at 6·period 5. T3 emits its first value once e6 is ready, since the output formula needs e3 through e6.

Edge cases

  • Constant series. Because c1 + c2 + c3 + c4 = 1 for any v, a flat input series produces a flat output equal to the constant (coefficients_sum_to_one and constant_series_yields_the_constant pin this).
  • v = 0. The coefficients become c1 = c2 = c3 = 0, c4 = 1, so T3 is exactly the third stage of the EMA cascade (zero_volume_factor_collapses_to_triple_cascaded_ema pins this).
  • NaN / infinity inputs. Non-finite inputs are silently dropped — the cascade is not advanced — and the previous valid value is returned.
  • Reset. t3.reset() clears all six EMAs and the cached value.

Examples

Rust

use wickra::{BatchExt, Indicator, T3};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let prices: Vec<f64> = (1..=40).map(f64::from).collect();
    let mut t3 = T3::new(3, 0.7)?;
    let out = t3.batch(&prices);
    println!("warmup_period = {}", t3.warmup_period());
    println!("first ready index = {:?}", out.iter().position(Option::is_some));
    Ok(())
}

Output:

warmup_period = 13
first ready index = Some(12)

T3(3, 0.7) warms up after 6·3 5 = 13 inputs, so the first non-None output sits at index 12. On a pure ramp the output then tracks the input trend with a smooth, near-constant offset.

Python

import numpy as np
import wickra as ta

t3 = ta.T3(5)  # v defaults to 0.7
prices = np.linspace(100.0, 140.0, 60)
out = t3.batch(prices)
print("warmup_period =", t3.warmup_period())
print("ready values:", np.count_nonzero(~np.isnan(out)))

Output:

warmup_period = 25
ready values: 36

Node

const ta = require('wickra');
const t3 = new ta.T3(5, 0.7);
const prices = Array.from({ length: 60 }, (_, i) => 100 + i);
console.log('warmupPeriod:', t3.warmupPeriod());
console.log('last:', t3.batch(prices).at(-1));

Interpretation

T3 is a "best of both" trend line — close to Tema in lag reduction but visibly smoother, because the six-EMA cascade filters noise the three-EMA Tema lets through. Use it as a single trend filter or as the slow leg of a crossover where you want a clean line. Raise v toward 1 for sharper turns (more overshoot), lower it toward 0 for maximum smoothness (v = 0 is just a triple EMA).

Common pitfalls

  • Treating v as optional outside Python. Only the Python binding defaults v to 0.7; the Rust, Node and WASM constructors require it.
  • Underestimating warmup. 6·period 5 grows fast — a T3(20) needs 115 bars before its first value.

References

Tim Tillson, "Better Moving Averages", Technical Analysis of Stocks & Commodities (1998). The six-EMA expansion and coefficient formulas here match Tillson's published derivation and TA-Lib's T3.

See also