D07-F03-A05 / Complete engineering topic

Commodity Channel Index (CCI)

A production-minded guide to Commodity Channel Index (CCI).

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

Catalog: D07-F03-A05 · 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. CCI 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 typical price, its 20-bar mean, and the mean absolute deviation of those same typical prices from that mean. CCI expresses current typical-price displacement in units of scaled mean deviation. It is not mathematically bounded. 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 high, low, 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

CCIt=TPtSMA20(TP)t0.015MD20,tCCI_t=\frac{TP_t-SMA_{20}(TP)_t}{0.015\,MD_{20,t}}
SymbolMeaningUnit / scale
TPₜ(High + Low + Close) / 3price units
SMAₙ(TP)Mean typical price in the trailing windowprice units
MDₙMean absolute deviation from that same meanprice units
CCIₜDisplacement divided by 0.015 × MDunbounded

Parameters are period=20, constant=0.015. Compute typical price, its 20-bar mean, and the mean absolute deviation of those same typical prices from that mean. The first ready point is price index 19 (the 20th bar). 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

CCI expresses current typical-price displacement in units of scaled mean deviation. It is not mathematically bounded. If the 20 typical prices have zero mean deviation, the denominator is zero and CCI is 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

Standard deviation produces a z-score, not CCI. Different price fields, periods, or scaling constants materially change output. 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 19 (2025-01-20T00:00:00Z, synthetic). No value below is a provider observation.

ComponentAuthor-derived value
Typical price103.47634600
TP mean104.32338222
Mean deviation1.37993573
0.015 × MD0.02069904
CCI-40.92152986

Independent substitution: (103.47634600 − 104.32338222) / 0.02069904 = -40.92152986 (numerator -0.84703622).

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 regimes101042.7897Rising, falling, quiet, and renewed-trend segments with deterministic shocks.
Comparison · persistent rise1010120.0550A persistent positive path shows saturation and smoothing lag without claiming a forecast.
Comparison · range cycle1010-72.3299A bounded oscillating path exposes repeated range relocation and threshold crossings.
Boundary · flat then shock425947.8184A 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.

CCI 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 replace mean absolute deviation with standard deviation, assume ±100 are hard limits, or mix adjusted closes with unadjusted highs and lows. 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 CCI 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

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

Commodity Channel Index (CCI)

  • Organization or authors: TA-Lib
  • Source type: Official technical documentation
  • Publication or effective date: Last page update 2008-03-22
  • Version: TA-Lib CCI documentation accessed 2026-07-26
  • URL: Commodity Channel Index (CCI)
  • Accessed: 2026-07-26
  • Jurisdiction: Platform convention; not jurisdiction-specific
  • Applicability: Donald Lambert attribution and maintained implementation reference
  • Supports: Attribution to Donald Lambert and authoritative implementation reference.
  • Limitations: The 0.015 constant is a scaling convention; it is not a probability calibration.

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

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 labcci labOpen full screen