D07-F03-A08 / Complete engineering topic

Connors RSI

A production-minded guide to Connors RSI.

D07 · TECHNICAL INDICATORS
D07-F03-A08Canonical / Tested / Open
D07 / D07-F03

Catalog: D07-F03-A08 · 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. ConnorsRSI is useful because it compresses a defined recent path, but its output only means what its inputs, windows, and initialization allow it to mean.

Average short RSI of price, RSI of signed consecutive up/down streak length, and the strict-lower percentile rank of today's return against the prior 100 returns. The composite combines price momentum, run persistence, and how unusual the latest one-bar return is relative to recent history. 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 strictly positive 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

CRSIt=RSI3(C)t+RSI2(streak)t+PercentRank100(ROC1)t3CRSI_t=\frac{RSI_3(C)_t+RSI_2(streak)_t+PercentRank_{100}(ROC_1)_t}{3}
SymbolMeaningUnit / scale
RSIpriceWilder RSI of close0 to 100
streakₜSigned consecutive up/down run lengthbars
RSIstreakWilder RSI of streak0 to 100
PercentRankStrict-lower rank of current return against prior N returns0 to 100
CRSIₜArithmetic mean of the three components0 to 100

Parameters are price_period=3, streak_period=2, rank_period=100. Average short RSI of price, RSI of signed consecutive up/down streak length, and the strict-lower percentile rank of today's return against the prior 100 returns. The first ready point is price index 101 after 100 prior one-bar returns exist. 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 composite combines price momentum, run persistence, and how unusual the latest one-bar return is relative to recent history. The rank is unavailable until 100 prior non-null returns exist; nonpositive closes are rejected because simple returns would be ill-defined for this contract. 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

Tie handling, inclusion of the current return, streak-flat behavior, and RSI seed conventions vary. This package uses prior-only rank, strict-lower ties, flat streak reset to zero, and Wilder RSI. 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 101 (2025-04-12T00:00:00Z, synthetic). No value below is a provider observation.

ComponentAuthor-derived value
Price RSI80.68135284
Streak6.00000000
Streak RSI98.25713478
One-bar return0.00086228
Percent rank52.00000000
Connors RSI76.97949587

Independent substitution: (80.68135284 + 98.25713478 + 52.00000000) / 3 = 76.97949587.

Published checkpoints round to 10 decimal places; teaching prose shows 8 decimals so the displayed arithmetic remains readable.

Compare four complete paths

ScenarioReady rowsUndefined rowsFinal outputTeaching purpose
Canonical · four regimes19070.7277Rising, falling, quiet, and renewed-trend segments with deterministic shocks.
Comparison · persistent rise19083.6667A persistent positive path shows saturation and smoothing lag without claiming a forecast.
Comparison · range cycle19023.5235A bounded oscillating path exposes repeated range relocation and threshold crossings.
Boundary · flat then shock19017.1084A 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.

ConnorsRSI component pipeline with canonical synthetic values

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 hide tie or window conventions, call the components independent, or treat extreme composite values as strategy performance evidence. 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 ConnorsRSI 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

ConnorsRSI 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.

Calculation flow

Purpose: expose the exact dependency that is warming up or undefined before ConnorsRSI is emitted.

Rendering system map…

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 notes

Expand the source trail, evidence role, and limitations behind the engineering choices.

ConnorsRelativeStrengthIndex.cs

  • Organization or authors: QuantConnect LEAN
  • Source type: Maintained open-source reference implementation
  • Publication or effective date: Current master branch at access
  • Version: QuantConnect LEAN master source accessed 2026-07-26
  • URL: ConnorsRelativeStrengthIndex.cs
  • Accessed: 2026-07-26
  • Jurisdiction: Software implementation; not jurisdiction-specific
  • Applicability: Three-component structure, streak state, and strict-lower rank against prior values
  • Supports: Maintained open-source reference for the three-component structure, streak state, and prior-window strict-lower percent rank.
  • Limitations: LEAN readiness and initial-window details are not silently adopted; this package freezes its own reproducible convention.

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.

Claim-role ledger

RoleMaterial content in this packageTreatment
Sourced factIndicator attribution, original-book identity, and maintained function/reference implementation identitySupported only to the limits recorded above
Implementation choiceParameters, seed, warm-up suppression, null behavior, tie handling, and displayed scaleFrozen in the canonical README and contract; never attributed universally
Synthetic teaching inputFour deterministic 120-row OHLC scenariosLabeled synthetic in datasets, examples, visuals, and playground
Author-derived calculationComponent traces, checkpoints, classifications, diagrams, and worked substitutionsReproducible from the published formula and fixture; not provider data
Empirical resultNoneNo 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.

momentum.ts
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}}
Full-height labconnors rsi labOpen full screen