Catalog: D07-F03-A03 · 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. StochRSI is useful because it compresses a defined recent path, but its output only means what its inputs, windows, and initialization allow it to mean.
Compute Wilder RSI first, locate that RSI inside its own 14-value range, then smooth the normalized result twice. This is momentum-of-momentum. It is more reactive than RSI because it normalizes RSI rather than price. 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 |
|---|---|---|
| RSIₜ | Wilder RSI at row t | 0 to 100 |
| RSImin / RSImax | Trailing stochastic-window RSI extrema | 0 to 100 |
| Raw %K | RSI location inside its own range | 0 to 100 |
| %K / %D | Successive SMA smoothing stages | 0 to 100 |
Parameters are rsi_period=14, stoch_period=14, smooth_k=3, smooth_d=3. Compute Wilder RSI first, locate that RSI inside its own 14-value range, then smooth the normalized result twice. The first ready point is raw %K at index 27; complete %D at index 31. 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
This is momentum-of-momentum. It is more reactive than RSI because it normalizes RSI rather than price. If all RSI values in the stochastic window are equal, normalization is undefined and the output remains unavailable. 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
Some libraries expose unsmoothed fast lines or different defaults. The selected chart-style contract is 14/14/3/3 with SMA smoothing. 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 31 (2025-02-01T00:00:00Z, synthetic). No value below is a provider observation.
| Component | Author-derived value |
|---|---|
| RSI | 67.51255607 |
| RSI low | 38.52755214 |
| RSI high | 74.27056960 |
| Raw StochRSI | 81.09277277 |
| Smoothed %K | 59.21882141 |
| Smoothed %D | 48.63888282 |
Independent substitution: %D = (40.55890883 + 46.13891822 + 59.21882141) / 3 = 48.63888282.
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 | 89 | 0 | 32.2480 | Rising, falling, quiet, and renewed-trend segments with deterministic shocks. |
| Comparison · persistent rise | 0 | 89 | unavailable | A persistent positive path shows saturation and smoothing lag without claiming a forecast. |
| Comparison · range cycle | 89 | 0 | 13.3094 | A bounded oscillating path exposes repeated range relocation and threshold crossings. |
| Boundary · flat then shock | 37 | 52 | 68.2643 | 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 a 0-1 provider series with this 0-100 scale, ignore the nested warm-up, or infer price direction from oscillator saturation. 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 StochRSI 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
StochRSI 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 StochRSI 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.
Stochastic RSI (STOCHRSI)
- 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 1994
- Version: TA-Lib STOCHRSI documentation accessed 2026-07-26
- URL: Stochastic RSI (STOCHRSI)
- Accessed: 2026-07-26
- Jurisdiction: Platform convention; not jurisdiction-specific
- Applicability: Attribution and algorithm identity; package parameters remain an explicit implementation choice
- Supports: Attribution to Tushar Chande and Stanley Kroll and bibliographic reference to The New Technical Trader.
- Limitations: Provider defaults vary; this package selects the chart-style 14/14/3/3 variant.
New Concepts in Technical Trading Systems
- Organization or authors: J. Welles Wilder / Windsor Books
- Source type: Original-author book; publisher bibliographic record
- Publication or effective date: 1978
- Version: Hardcover, 141 pages; publisher page accessed 2026-07-26
- URL: New Concepts in Technical Trading Systems
- Accessed: 2026-07-26
- Jurisdiction: General market-analysis method; not jurisdiction-specific
- Applicability: Original RSI attribution and the existence of the Relative Strength Index chapter
- Supports: Primary bibliographic record for Wilder's 1978 book and its Relative Strength Index chapter.
- Limitations: The publisher page is bibliographic; the implementation choices are documented and tested in this package.
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.