TSI — True Strength Index
Momentum smoothed twice, divided by absolute momentum smoothed the same way. The double smoothing removes most of the noise from raw price change while the normalisation keeps the result on a −100 to +100 scale; the signal line makes it a crossover system.
Signature
TSI(long_period, short_period, signal_period, on=<source>)| Argument | Type | Required | Notes |
|---|---|---|---|
long_period |
int Param or literal | Yes | First smoothing. Default 25, bounds 1–200. |
short_period |
int Param or literal | Yes | Second smoothing. Default 13, bounds 1–200. |
signal_period |
int Param or literal | Yes | EMA of the TSI. Default 13, bounds 1–200. |
on= |
symbol, OHLCV field, or indicator | No | Source. Defaults to the trade symbol’s close. |
Formula
$$ \text{TSI} = 100,\frac{\text{EMA}(\text{EMA}(\Delta C, r), s)}{\text{EMA}(\text{EMA}(|\Delta C|, r), s)} $$
Warmup
Both EMA chains start at zero and converge over roughly 3 × long_period bars.
Reading the value
tsi.tsitsi.signaltsi.tsi > tsi.signalMulti-value: cannot be chained onto or crossed.
Example
class TsiCross(Strategy): """Long while TSI leads its signal line above zero.""" trade = Symbol() sizing = FixedCapital()
def execute(self): tsi = TSI(25, 13, 13, on=self.trade) if tsi.tsi > tsi.signal and tsi.tsi > 0 and self.trade.is_flat: self.buy(self.trade, reason=f"TSI {tsi.tsi:.1f}") if tsi.tsi < tsi.signal and self.trade.is_long: self.sell(self.trade, self.trade.position, reason="TSI below signal")Notes
- William Blau, Momentum, Direction and Divergence (1995).
- Proven by an assert-via-trade run on SPY daily (converged to an independent double-EMA replication within 0.5 once warm; zero alarm trades).
SMIErgodicis the same construction on a 0–1 scale with different defaults.- The recursion seeds from the first bar rather than from a warm-up window, so early values
are not comparable with pandas_ta’s
tsi(13, 25, 13), which seeds from an average of the firstn; measured on a 300-bar tape the two agree to 1e-6 from bar 200 onward.