Catalog: D07-F03-A07 · Family: D07-F03 Momentum · Status: publication-ready
The question this indicator actually answers
Technical indicators become dangerous when a familiar label replaces a precise question. TSI is useful because it compresses a defined recent path, but its output only means what its inputs, windows, and initialization allow it to mean.
Double-smooth signed close changes and their absolute magnitudes, then divide the signed path by the magnitude path. The sign shows the direction of double-smoothed change; magnitude shows directional dominance under the same recursive filter. That sentence is the center of the tutorial: measurement first, interpretation second, trading rules elsewhere.
Freeze the data before the formula
The selected input is finalized close. Bars must be strictly ordered and finalized, use one interval and session policy, and share one price-adjustment basis. If a split is handled in close but not high and low, a range oscillator can become mathematically correct over economically inconsistent fields. If a missing bar is silently filled, the lookback describes a different time path. The reference function therefore validates numeric structure and leaves timestamps, calendars, adjustments, and finality to the caller.
The canonical fixture contains 120 author-generated UTC daily bars. It is intentionally synthetic. It can prove arithmetic, warm-up, parity, and causal behavior; it cannot prove a market edge.
The selected calculation
| Symbol | Meaning | Unit / scale |
|---|---|---|
| ΔCₜ | Current close minus previous close | price units |
| EMAₗ(ΔC) | Long EMA of signed change | price units |
| EMAₛ(EMAₗ(ΔC)) | Double-smoothed signed change | price units |
| TSIₜ | Signed double EMA divided by absolute-change double EMA | -100 to 100 |
Parameters are long=25, short=13. Double-smooth signed close changes and their absolute magnitudes, then divide the signed path by the magnitude path. The first ready point is price index 37 after the declared suppression window. Every earlier row remains visibly unavailable rather than being replaced by zero. Zero is a valid oscillator value for some indicators, so using it as a warm-up sentinel would corrupt both charts and downstream rules.
Read the scale carefully
The sign shows the direction of double-smoothed change; magnitude shows directional dominance under the same recursive filter. If the double-smoothed absolute change is zero, TSI is unavailable rather than fabricated as neutral. A threshold can be a convenient classification boundary, but it is not a probability calibration. A reading in a commonly named zone does not specify an entry time, order type, holding period, stop, cost model, or expected return.
The variant trap
Platforms differ on the long/short ordering, EMA seeding, warm-up suppression, and optional signal line. This package freezes 25 then 13, first-change seeds, and no signal line. When two platforms disagree, compare source field, parameters, smoothing type, seed, warm-up suppression, range scale, and denominator policy before assuming either is broken. Matching the indicator name is not enough.
Reproduce one complete row
The first complete canonical row is zero-based index 37 (2025-02-07T00:00:00Z, synthetic). No value below is a provider observation.
| Component | Author-derived value |
|---|---|
| Change | 0.83914700 |
| EMA long ΔC | 0.25249286 |
| Double EMA ΔC | 0.20010805 |
| EMA long | ΔC |
| Double EMA | ΔC |
| TSI | 25.14864941 |
Independent substitution: 100 × 0.20010805 / 0.79570096 = 25.14864941.
Published checkpoints round to 10 decimal places; teaching prose shows 8 decimals so the displayed arithmetic remains readable.
Compare four complete paths
| Scenario | Ready rows | Undefined rows | Final output | Teaching purpose |
|---|---|---|---|---|
| Canonical · four regimes | 83 | 0 | 20.4375 | Rising, falling, quiet, and renewed-trend segments with deterministic shocks. |
| Comparison · persistent rise | 83 | 0 | 100.0000 | A persistent positive path shows saturation and smoothing lag without claiming a forecast. |
| Comparison · range cycle | 83 | 0 | -29.2209 | A bounded oscillating path exposes repeated range relocation and threshold crossings. |
| Boundary · flat then shock | 41 | 42 | 16.2710 | A long zero-movement prefix exposes neutral or undefined denominators before a controlled shock. |
These scenarios are not cherry-picked trades. They isolate mechanics: multi-regime behavior, persistent direction, repeated range movement, and a zero-movement boundary followed by a controlled shock. Python and TypeScript consume the same canonical JSON checkpoints, while the playground exposes the enriched component trace for all scenario/preset combinations.
See the component pipeline
What to notice: each box names a state that is visible in the playground audit table. If the final output is unavailable, the status panel identifies whether the missing stage is causal warm-up or an undefined denominator.
Open the component pipeline at full size.
Implementation and operational behavior
The reference code favors transparency and aligned nulls. The enriched fixture adds author-derived component traces without changing the reference function's public calculation. The executable examples print audited ready rows. A production streaming implementation can replace repeated window scans with monotonic queues or running sums, but must preserve the exact same clock, seed, and invalid-state semantics.
Revision policy matters. Rolling indicators require recalculation until the revised row leaves the largest window. Recursive indicators can alter the entire suffix. Store the code version and parameters with the published series.
What can go wrong
Do not compare unmatched seeds, call a zero cross a guaranteed trend change, or ignore that historical revisions alter the recursive suffix. Also avoid hindsight-defined divergence, thresholds optimized on the evaluation sample, and provisional-bar signals evaluated against finalized-bar outcomes. These are research-design failures, not formula failures.
Combining several momentum indicators does not automatically create independent evidence. RSI, StochRSI, Williams %R, and Stochastic can reuse the same close path and overlapping windows. Correlated transforms may create a false impression of confirmation.
Appropriate real-world use
Use TSI as a reproducible chart layer, a monitored feature, or one input to a separately governed research pipeline. Define any event rule after the measurement: crossing direction, tolerance, bar finality, action time, costs, and risk controls all belong outside the indicator.
Why the historical example is deferred
A named security would require authoritative or licensed timestamped observations, stable identifier, time zone and session semantics, adjusted-or-unadjusted basis, corporate-action and correction lineage, access date, and redistribution rights. It would also require a clean separation between provider observations and author-derived values. Until that evidence bundle exists, a named example would add realism at the cost of provenance. The synthetic case is the stronger verification artifact today.
Takeaway
TSI is best understood as a contract: declared data, declared transformation, declared warm-up, and declared limits. Preserve that contract and the output is reproducible. Add trading meaning without a separate rule and evidence process, and precision turns into false confidence.
Sources
See ../REFERENCES.md for the claim-level source ledger and limits.
Rendered from the canonical Mermaid sources linked by this article.
Calculation flow
Purpose: expose the exact dependency that is warming up or undefined before TSI is emitted.
Interpretation: a null result is not enough information by itself. The enriched fixture and lab preserve the component trace plus a reason that distinguishes causal warm-up from an undefined scale.
Takeaway: availability gates are part of the algorithm, not presentation cleanup.
ReferencesPrimary sources and evidence notesExpand the source trail, evidence role, and limitations behind the engineering choices.
Expand the source trail, evidence role, and limitations behind the engineering choices.
True Strength Index (TSI)
- Organization or authors: TA-Lib
- Source type: Official technical documentation and original-book bibliography
- Publication or effective date: Last page update 2008-03-22; original book 1995
- Version: TA-Lib TSI documentation accessed 2026-07-26
- URL: True Strength Index (TSI)
- Accessed: 2026-07-26
- Jurisdiction: Platform convention; not jurisdiction-specific
- Applicability: William Blau attribution and algorithm identity
- Supports: Attribution to William Blau and bibliographic reference to Momentum, Direction, and Divergence.
- Limitations: EMA initialization and signal-line choices vary across platforms.
Momentum, Direction, and Divergence
- Organization or authors: William Blau / Wiley
- Source type: Original-author book; publisher bibliographic record
- Publication or effective date: 1995-03
- Version: First edition; publisher catalog accessed 2026-07-26
- URL: Momentum, Direction, and Divergence
- Accessed: 2026-07-26
- Jurisdiction: General market-analysis method; not jurisdiction-specific
- Applicability: Bibliographic support for Momentum, Direction, and Divergence
- Supports: Publisher bibliographic record for William Blau's 1995 book.
- Limitations: The catalog page is bibliographic rather than a complete formula specification.
TA-Lib Technical Analysis Documentation
- Organization or authors: TA-Lib
- Source type: Maintained official technical documentation index
- Publication or effective date: Documentation index current at access
- Version: Web documentation accessed 2026-07-26
- URL: TA-Lib Technical Analysis Documentation
- Accessed: 2026-07-26
- Jurisdiction: Platform convention; not jurisdiction-specific
- Applicability: Function identity, attribution, and links to maintained implementation references
- Supports: Author and function index for RSI, Stochastic, Stochastic RSI, CCI, Ultimate Oscillator, TSI, and Williams %R.
- Limitations: The index is a reference map; every package separately freezes formulas, seeds, warm-up, and zero-denominator behavior.
Claim-role ledger
| Role | Material content in this package | Treatment |
|---|---|---|
| Sourced fact | Indicator attribution, original-book identity, and maintained function/reference implementation identity | Supported only to the limits recorded above |
| Implementation choice | Parameters, seed, warm-up suppression, null behavior, tie handling, and displayed scale | Frozen in the canonical README and contract; never attributed universally |
| Synthetic teaching input | Four deterministic 120-row OHLC scenarios | Labeled synthetic in datasets, examples, visuals, and playground |
| Author-derived calculation | Component traces, checkpoints, classifications, diagrams, and worked substitutions | Reproducible from the published formula and fixture; not provider data |
| Empirical result | None | No association, prediction, profitability, or strategy-performance claim is published |
Evidence boundary
The scenarios are author-generated synthetic teaching data. Formulas, outputs, component traces, checkpoints, diagrams, and interpretations derived from them are author calculations. No fixture row is a provider observation, no threshold is presented as a probability, and no trading-performance claim is made.
A historical example is deferred. Publication would require a traceable instrument identifier, licensed or authoritative timestamped OHLC data, session/time-zone rules, adjusted-or-unadjusted basis, corporate-action lineage, exact parameters and seeds, access date, and reproducible provider-versus-author calculation separation.
Full dependency-light reference implementations in both supported languages.
export type Maybe = number | null;
const period = (v:number,n:string) => { if(!Number.isInteger(v)||v<=0) throw new Error(`${n} must be a positive integer`); };
const series = (xs:number[],n:string,positive=false) => { if(!xs.length||xs.some(v=>!Number.isFinite(v)||(positive&&v<=0))) throw new Error(`${n} contains invalid values`); return xs.map(Number); };
const aligned = (h:number[],l:number[],c:number[]) => { const H=series(h,"high"),L=series(l,"low"),C=series(c,"close"); if(H.length!==L.length||H.length!==C.length) throw new Error("inputs must align"); H.forEach((v,i)=>{if(L[i]>Math.min(v,C[i])||v<Math.max(L[i],C[i]))throw new Error("invalid bar")}); return [H,L,C]; };
const sma = (x:Maybe[],p:number) => x.map((_,i)=>{if(i<p-1)return null; const w=x.slice(i-p+1,i+1); return w.every(v=>v!==null) ? (w as number[]).reduce((a,b)=>a+b,0)/p : null;});
const rsiValues = (values:number[],p:number):Maybe[] => { period(p,"period"); const x=series(values,"values"),out:Maybe[]=Array(x.length).fill(null); if(x.length<=p)return out; let g=0,l=0; for(let i=1;i<=p;i++){const d=x[i]-x[i-1];g+=Math.max(d,0);l+=Math.max(-d,0)} g/=p;l/=p; const val=(a:number,b:number)=>a===0&&b===0?50:b===0?100:a===0?0:100-100/(1+a/b); out[p]=val(g,l); for(let i=p+1;i<x.length;i++){const d=x[i]-x[i-1];g=(g*(p-1)+Math.max(d,0))/p;l=(l*(p-1)+Math.max(-d,0))/p;out[i]=val(g,l)} return out; };
export function rsi(close:number[],p=14){return {rsi:rsiValues(close,p)}}
export function stochastic(high:number[],low:number[],close:number[],kp=14,sk=3,sd=3){period(kp,"k_period");period(sk,"smooth_k");period(sd,"smooth_d");const [h,l,c]=aligned(high,low,close),fast:Maybe[]=Array(c.length).fill(null);for(let i=kp-1;i<c.length;i++){const hh=Math.max(...h.slice(i-kp+1,i+1)),ll=Math.min(...l.slice(i-kp+1,i+1));if(hh!==ll)fast[i]=100*(c[i]-ll)/(hh-ll)}const slow=sma(fast,sk);return{fast_k:fast,slow_k:slow,slow_d:sma(slow,sd)}}
export function stochastic_rsi(close:number[],rp=14,sp=14,sk=3,sd=3){period(sp,"stoch_period");period(sk,"smooth_k");period(sd,"smooth_d");const values=rsiValues(close,rp),raw:Maybe[]=Array(values.length).fill(null);for(let i=sp-1;i<values.length;i++){const w=values.slice(i-sp+1,i+1);if(w.every(v=>v!==null)){const a=w as number[],hi=Math.max(...a),lo=Math.min(...a);if(hi!==lo)raw[i]=100*((values[i] as number)-lo)/(hi-lo)}}const k=sma(raw,sk);return{rsi:values,raw_k:raw,k,d:sma(k,sd)}}
export function williams_r(high:number[],low:number[],close:number[],p=14){period(p,"period");const[h,l,c]=aligned(high,low,close),out:Maybe[]=Array(c.length).fill(null);for(let i=p-1;i<c.length;i++){const hh=Math.max(...h.slice(i-p+1,i+1)),ll=Math.min(...l.slice(i-p+1,i+1));if(hh!==ll)out[i]=-100*(hh-c[i])/(hh-ll)}return{williams_r:out}}
export function cci(high:number[],low:number[],close:number[],p=20,k=0.015){period(p,"period");if(!Number.isFinite(k)||k<=0)throw new Error("constant must be positive");const[h,l,c]=aligned(high,low,close),tp=c.map((v,i)=>(h[i]+l[i]+v)/3),out:Maybe[]=Array(c.length).fill(null);for(let i=p-1;i<c.length;i++){const w=tp.slice(i-p+1,i+1),m=w.reduce((a,b)=>a+b,0)/p,d=w.reduce((a,b)=>a+Math.abs(b-m),0)/p;if(d!==0)out[i]=(tp[i]-m)/(k*d)}return{typical_price:tp,cci:out}}
export function ultimate_oscillator(high:number[],low:number[],close:number[],s=7,m=14,g=28){[["short",s],["medium",m],["long",g]].forEach(([n,v])=>period(v as number,n as string));if(!(s<m&&m<g))throw new Error("period order");const[h,l,c]=aligned(high,low,close),bp:Maybe[]=Array(c.length).fill(null),tr:Maybe[]=Array(c.length).fill(null),out:Maybe[]=Array(c.length).fill(null);for(let i=1;i<c.length;i++){const floor=Math.min(l[i],c[i-1]),ceiling=Math.max(h[i],c[i-1]);bp[i]=c[i]-floor;tr[i]=ceiling-floor}for(let i=g;i<c.length;i++){const ratios:number[]=[];let ok=true;for(const p of [s,m,g]){const b=(bp.slice(i-p+1,i+1) as number[]).reduce((a,v)=>a+v,0),t=(tr.slice(i-p+1,i+1) as number[]).reduce((a,v)=>a+v,0);if(t===0){ok=false;break}ratios.push(b/t)}if(ok)out[i]=100*(4*ratios[0]+2*ratios[1]+ratios[2])/7}return{buying_pressure:bp,true_range:tr,ultimate_oscillator:out}}
const emaFirst=(x:Maybe[],p:number)=>{const a=2/(p+1),out:Maybe[]=Array(x.length).fill(null);let prev:number|null=null;x.forEach((v,i)=>{if(v===null)return;prev=prev===null?v:prev+a*(v-prev);out[i]=prev});return out};
export function tsi(close:number[],g=25,s=13){period(g,"long");period(s,"short");const c=series(close,"close"),d:Maybe[]=[null,...c.slice(1).map((v,i)=>v-c[i])],n=emaFirst(emaFirst(d,g),s),q=emaFirst(emaFirst(d.map(v=>v===null?null:Math.abs(v)),g),s),out:Maybe[]=Array(c.length).fill(null);for(let i=g+s-1;i<c.length;i++)if(q[i]!==null&&q[i]!==0)out[i]=100*(n[i] as number)/(q[i] as number);return{tsi:out}}
export function connors_rsi(close:number[],pp=3,sp=2,rp=100){period(pp,"price_period");period(sp,"streak_period");period(rp,"rank_period");const c=series(close,"close",true),price=rsiValues(c,pp),streak:number[]=Array(c.length).fill(0);for(let i=1;i<c.length;i++){if(c[i]>c[i-1])streak[i]=streak[i-1]>0?streak[i-1]+1:1;else if(c[i]<c[i-1])streak[i]=streak[i-1]<0?streak[i-1]-1:-1}const srsi=rsiValues(streak,sp),roc:Maybe[]=[null,...c.slice(1).map((v,i)=>(v-c[i])/c[i])],rank:Maybe[]=Array(c.length).fill(null),out:Maybe[]=Array(c.length).fill(null);for(let i=rp+1;i<c.length;i++){const prior=roc.slice(i-rp,i) as number[];rank[i]=100*prior.filter(v=>v<(roc[i] as number)).length/rp;if(price[i]!==null&&srsi[i]!==null)out[i]=((price[i] as number)+(srsi[i] as number)+(rank[i] as number))/3}return{price_rsi:price,streak,streak_rsi:srsi,percent_rank:rank,connors_rsi:out}}
The embedded lab now expands to its full document height, keeping the article as the only scroll surface.