The governed definitions this build depends on. Read them first if a term is unfamiliar.
- PrimaryRatio-Adjusted McClellan OscillatorA McClellan oscillator calculated from 1000 times net advances divided by advancing plus declining issues.
- PrerequisiteExponential Moving AverageAn Exponential Moving Average recursively gives recent observations greater influence than progressively older observations.
- PrerequisiteNet AdvancesNet Advances is the number of advancing issues minus the number of declining issues for one validated session.
- PrerequisiteTraditional McClellan OscillatorA breadth oscillator equal to the 10% Trend minus the 5% Trend of daily advancing issues minus declining issues.
- Important10% TrendThe faster McClellan exponential trend that assigns 10 percent weight to the current breadth input.
- Important5% TrendThe slower McClellan exponential trend that assigns 5 percent weight to the current breadth input.
- ImportantBreadth Scale FactorThe breadth scale factor multiplies a normalized issue-balance ratio into a declared reporting scale.
- ImportantCausal Knowledge CutoffThe time boundary that prevents information unavailable at that moment from influencing a calculation.
- ImportantEMA Period-Label EquivalenceEMA period-label equivalence maps a conventional period label to its smoothing constant.
- ImportantMoving IssuesMoving Issues is the sum of advancing and declining issue counts for a session.
- ImportantNo-Imputation Missing-Session PolicyA policy that blocks or pauses calculation instead of converting a missing required session into a zero observation.
- ImportantOscillatorAn indicator constructed to fluctuate around a reference level or within a range as its inputs change.
- ImportantProcessing CheckpointA persisted valid state and provenance bundle from which deterministic processing can safely resume.
- ImportantRatio-Adjusted BreadthRatio-adjusted breadth scales advances minus declines by advances plus declines.
- ImportantReady PointThe first eligible observation at which every state required for an indicator output has been initialized.
- ImportantRecursive StateA stored calculation value that a recurrence carries from one accepted observation into the next update.
- ImportantRevision LineageAn ordered chain connecting each correction to the exact earlier record it supersedes.
- ImportantSMA-Seeded EMA InitializationAn EMA initialization policy that uses the arithmetic mean of the first eligible observations as the initial recursive state.
- ImportantSmoothing ConstantA smoothing constant controls how far an exponential state moves toward each new observation.
- ImportantSuffix RecalculationRecomputing a corrected observation and every later output whose recursive state may depend on it.
- ImportantWarm-Up PeriodThe initial span in which an indicator is accumulating enough eligible history to satisfy its declared initialization policy.
- ImportantZero-Mover SessionA zero-mover session has no advancing or declining issues, so A plus D equals zero.
Is breadth momentum changing, after removing the mechanical effect of a larger or smaller issue universe? Ratio-Adjusted McClellan Oscillator answers that question only when the input unit, warm-up, seed, and evidence cutoff remain explicit.
The numerical path below is synthetic teaching data. The methodology statements are sourced; every displayed input, EMA, oscillator, and index value is author-derived.
The decision before the formula
Choose this oscillator when issue-count scale comparability matters but you do not need a cumulative index state. Normalization changes the unit from issues to breadth points per 1,000 moving issues.
The six-topic McClellan family has three input choices—raw issue counts, ratio-adjusted issue breadth, and raw allocated volume—and two output choices—an oscillator or a cumulative Summation Index. Sharing the 10%/5% engine does not make those choices interchangeable.
| Question | This topic’s answer |
|---|---|
| What is observed? | Advancing and declining issues, normalized to 1,000 moving issues |
| What memory is retained? | Dual EMA state only |
| What happens when source scale doubles? | Doubling both advances and declines leaves the ratio-adjusted input and oscillator unchanged; the raw-count sibling doubles. |
| What is withheld? | A+D is zero, so the normalized input is undefined. |
From evidence to input
The package first resolves one ready revision for every required session at the requested knowledge cutoff. Missing, cancelled, provisional, duplicated, or identity-incompatible evidence is not converted into a plausible zero.
Only after that resolution does the topic transform the record:
Then the shared tracking engine updates:
The 10% and 5% tracking rates correspond to the conventional 19- and 39-period labels because
The labels do not turn exponential memory into finite windows. Old observations decay; they do not disappear exactly.
The important boundary is left to right: source resolution precedes recursion, and accumulation—when present—follows a ready oscillator.
Warm-up is observable state
The package uses the simple average of observations 1–19 as the first fast state and the simple average of observations 1–39 as the first slow state. Before observation 39, the oscillator is null because both states do not yet coexist.
This topic stops at the oscillator. Accumulation belongs to its Summation Index sibling.
The canonical cross-language fixture uses a balanced warm-up because it makes exact parity checks easy. The guided lab uses a separate nonzero path to teach the state transition instead of opening on a blank chart.
A nonzero 60-session teaching path
The lab moves through a broad advance, cooling, reacceleration, reversal, and stress/recovery regime.
| Observation | Regime | Input | Fast | Slow | Oscillator | Index |
|---|---|---|---|---|---|---|
| 39 | reversal | 235.2941 | 10.8538 | 69.1973 | -58.3435 | — |
| 40 | stress and recovery | 333.3333 | 43.1017 | 82.4041 | -39.3023 | — |
| 45 | stress and recovery | -266.6667 | 103.6062 | 112.1741 | -8.5679 | — |
| 60 | stress and recovery | 55.5556 | 37.3116 | 58.1002 | -20.7886 | — |
At observation 39, the fast and slow states are already different. That makes the first ready oscillator a genuine state comparison rather than a zero placeholder. By observation 45, the current input and the two memory states reveal how the algorithm reacts to a regime change.
Notice the sequence, not merely the last number: the input changes first, the fast state reacts more, the slow state follows, and the output records their separation.
The scale comparison that distinguishes this topic
The lab can double the source scale without changing the direction pattern.
| Source scale at observation 45 | Input | Oscillator | Index |
|---|---|---|---|
| 1× | -266.6667 | -8.5679 | — |
| 2× | -266.6667 | -8.5679 | — |
Doubling both advances and declines leaves the ratio-adjusted input and oscillator unchanged; the raw-count sibling doubles. This is the family-level reason to name the input unit before interpreting magnitude.
The highlighted cell is this topic. Move horizontally to change the input unit; move vertically to add or remove cumulative memory.
Data contract: a number is not enough
Issue-count topics require a reconciled universe partition and stable classification method. Ratio-adjusted topics also require a positive advances-plus-declines denominator. Volume topics require nonnegative Up, Down, and Unchanged Volume that reconcile exactly to positive Total Volume, with stable venue, unit, and adjustment basis.
Every record carries session order, effective time, availability time, revision, evidence state, and series identity. A correction changes the dependent EMA suffix; a cumulative topic also changes every later index value.
Failure is a visible result
The failure scenario stops at observation 45 and reports why the next update is withheld: A+D is zero, so the normalized input is undefined. The chart preserves the last valid path but does not draw a fictional recursive suffix.
This distinction matters operationally:
- a validated input of zero updates both EMA states;
- a missing or provisional observation performs no update;
- a broken partition is invalid, not neutral;
- a future revision cannot change an earlier knowledge-cutoff result;
- a series-identity change starts a new governed series.
Implementation without hidden assumptions
The reference code separates three responsibilities:
- resolve evidence at the causal cutoff;
- transform one validated record into the topic’s declared input unit;
- update unrounded fast, slow, oscillator, and optional cumulative state.
That separation makes a production review easier: a correct subtraction cannot conceal a broken source partition, and a correct recurrence cannot conceal future information.
Python and TypeScript share the same canonical fixture and reason-code semantics. The teaching dataset is intentionally separate, so richer pedagogy does not turn the implementation test fixture into presentation data.
Evidence boundary and historical decision
McClellan Financial’s first-party material supports the 10%/5% engine and the topic-specific transform or accumulation rule. The package’s SMA initialization, software statuses, causal revision schema, teaching seed, and synthetic scenarios are explicit implementation choices.
A named historical example is deferred for this topic. A reproducible case would require the exact point-in-time universe or volume allocation, source timestamps, corrections, adjustment basis, calibration lineage, and redistribution permission. A synthetic path is better evidence of the mechanism than an incompletely specified market number.
Methodology pages also contain interpretive language and historical thresholds. Those statements are not promoted here into universal trading rules, predictive claims, or evidence of profitability.
Explore the guided lab
Open the guided playground, then:
- inspect the nonzero observation-39 state;
- step through the stress/recovery regime;
- select the expanded-source comparison;
- watch the scale diagnostic explain what changed and what stayed invariant;
- select invalid evidence and step to observation 45;
- reset and confirm the same deterministic state.
Summary
Ratio-Adjusted McClellan Oscillator is not defined only by fast minus slow. It is defined by its input unit, its two initialization states, its optional cumulative seed, and its evidence boundary. Doubling both advances and declines leaves the ratio-adjusted input and oscillator unchanged; the raw-count sibling doubles.
Next: Traditional McClellan Summation Index.
Primary references
See the annotated source record and the canonical technical contract.
Rendered from the canonical Mermaid sources linked by this article.
Ratio-Adjusted McClellan Oscillator calculation flow
Purpose: show that evidence, input units, recursive memory, and optional cumulative state are separate decisions.
Takeaway: Doubling both advances and declines leaves the ratio-adjusted input and oscillator unchanged; the raw-count sibling doubles.
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.
Only the formulas and historical naming are sourced facts. The fixture is synthetic; its arithmetic, seed choice, validation schema, error states, and software interfaces are explicit author choices.
MCO-01 — Calculating the McClellan Oscillator
- Organization or authors: McClellan Financial Publications; Tom McClellan
- Source type: Maintained first-party methodology note
- Publication or effective date: Not stated unless shown on the source page
- Version: Web or PDF edition accessed 2026-07-25
- URL: Calculating the McClellan Oscillator
- Accessed: 2026-07-25
- Jurisdiction: United States market-breadth methodology
- Supports: 10% and 5% EMA recurrences, raw A-D input, oscillator subtraction, and the 19/39 label equivalence.
- Limitations: It does not prescribe this package's SMA seed, causal cutoff, revision schema, or software error states.
MCO-02 — Ratio Adjusted Summation Index
- Organization or authors: McClellan Financial Publications; Tom McClellan
- Source type: Maintained first-party methodology note
- Publication or effective date: Not stated unless shown on the source page
- Version: Web or PDF edition accessed 2026-07-25
- URL: Ratio Adjusted Summation Index
- Accessed: 2026-07-25
- Jurisdiction: United States market-breadth methodology
- Supports: The adjustment 1000(A-D)/(A+D), exclusion of unchanged issues, and continuation through the ordinary 10%/5% calculation.
- Limitations: Interpretive thresholds are historical practice, not universal or validated trading rules.
NASD-01 — A-D definition
- Organization or authors: Nasdaq, Inc.
- Source type: Official exchange glossary
- Publication or effective date: Not stated unless shown on the source page
- Version: Web or PDF edition accessed 2026-07-25
- URL: A-D definition
- Accessed: 2026-07-25
- Jurisdiction: United States market-breadth methodology
- Supports: Advancing issues minus declining issues as a signed market-breadth quantity.
- Limitations: It does not define this package's universe, volume allocation, revisions, or EMA initialization.
Evidence reconciliation
- Sourced fact: McClellan Financial's first-party RASI note defines the A-D ratio adjustment and says the ordinary 10%/5% calculation then proceeds as usual.
- Implementation choice: SMA seeds at observations 19 and 39, display-only rounding, atomic withholding for unresolved evidence, and the documented accumulator seed.
- Synthetic teaching input: the 60-session fixture under
datasets/. - Author-derived calculation: every transformed input, EMA, oscillator, and cumulative value in the worked example.
- Historical-example decision: deferred. A named market series would require a reproducible universe, final breadth or volume allocation, revision history, calibration history, access timestamp, and redistribution permission.
- Empirical claim intentionally excluded: passing calculation tests does not establish forecasting or trading value.
Full dependency-light reference implementations in both supported languages.
/** Reference implementation for Ratio-Adjusted McClellan Oscillator. */
export type RecordRow = Record<string, unknown>;
export const MODE = "ratio_issue";
export const INITIAL_INDEX: number | null = null;
const FAST_ALPHA = 0.10;
const SLOW_ALPHA = 0.05;
const IDENTITY_FIELDS = ["venue_id", "universe_id", "methodology_id", "calendar_id", "volume_unit", "volume_adjustment_basis"] as const;
function numberValue(value: unknown, name: string): number {
if (typeof value !== "number" || !Number.isFinite(value)) throw new Error(`${name} must be a finite number`);
return value;
}
function timestamp(value: unknown, name: string): number {
if (typeof value !== "string" || !value.endsWith("Z")) throw new Error(`${name} must be an RFC 3339 UTC timestamp ending in Z`);
const parsed = Date.parse(value);
if (!Number.isFinite(parsed)) throw new Error(`${name} is invalid`);
return parsed;
}
export function transformRecord(record: RecordRow): number {
if (MODE.startsWith("ratio_issue")) {
const advances = numberValue(record.advances, "advances");
const declines = numberValue(record.declines, "declines");
if (advances < 0 || declines < 0) throw new Error("issue counts must be non-negative");
const denominator = advances + declines;
if (denominator === 0) throw new Error("ratio-adjusted breadth is undefined when advances + declines is zero");
return 1000 * (advances - declines) / denominator;
}
if (MODE.startsWith("raw_issue")) {
const advances = numberValue(record.advances, "advances");
const declines = numberValue(record.declines, "declines");
if (advances < 0 || declines < 0) throw new Error("issue counts must be non-negative");
return advances - declines;
}
const up = numberValue(record.up_volume, "up_volume");
const down = numberValue(record.down_volume, "down_volume");
const unchanged = numberValue(record.unchanged_volume, "unchanged_volume");
const total = numberValue(record.total_volume, "total_volume");
if (Math.min(up, down, unchanged, total) < 0) throw new Error("volume fields must be non-negative");
if (total <= 0 || up + down + unchanged !== total) throw new Error("volume partition must be positive and reconcile exactly");
return up - down;
}
export function calculateValues(values: readonly number[]) {
const data = values.map((value) => numberValue(value, "input value"));
let fast: number | null = null;
let slow: number | null = null;
let indexState = INITIAL_INDEX;
return data.map((value, offset) => {
const position = offset + 1;
if (position === 19) fast = data.slice(0, 19).reduce((a, b) => a + b, 0) / 19;
else if (position > 19) fast = fast! + FAST_ALPHA * (value - fast!);
if (position === 39) slow = data.slice(0, 39).reduce((a, b) => a + b, 0) / 39;
else if (position > 39) slow = slow! + SLOW_ALPHA * (value - slow!);
const oscillator = slow === null ? null : fast! - slow;
let indexValue: number | null = null;
if (oscillator !== null && INITIAL_INDEX !== null) {
indexState = indexState! + oscillator;
indexValue = indexState;
}
return {
observation: position, input_value: value, fast_ema: fast, slow_ema: slow,
oscillator, index_value: indexValue,
status: position < 19 ? "warming_fast" : position < 39 ? "warming_slow" : "ready",
};
});
}
export function calculate(records: readonly RecordRow[], cutoff: string) {
const cutoffTime = timestamp(cutoff, "cutoff");
const retained: RecordRow[] = [];
let ignoredFuture = 0;
for (const record of records) {
if (timestamp(record.available_at, "available_at") > cutoffTime) ignoredFuture += 1;
else retained.push(record);
}
if (!retained.length) return {status: "incomplete", reason_codes: ["no_evidence_at_cutoff"], points: [], ignored_future_records: ignoredFuture};
const groups = new Map<number, RecordRow[]>();
for (const record of retained) {
const sequence = record.session_sequence;
const revision = record.revision;
if (!Number.isInteger(sequence) || (sequence as number) <= 0) throw new Error("session_sequence must be a positive integer");
if (!Number.isInteger(revision) || (revision as number) < 0) throw new Error("revision must be a non-negative integer");
const bucket = groups.get(sequence as number) ?? [];
bucket.push(record);
groups.set(sequence as number, bucket);
}
const sequences = [...groups.keys()].sort((a, b) => a - b);
for (let i = 1; i < sequences.length; i += 1) {
if (sequences[i] !== sequences[i - 1] + 1) return {status: "incomplete", reason_codes: ["missing_session_sequence"], points: [], ignored_future_records: ignoredFuture};
}
const resolved: RecordRow[] = [];
let identity: string | null = null;
for (const sequence of sequences) {
const revisions = groups.get(sequence)!;
const numbers = revisions.map((row) => row.revision as number);
if (new Set(numbers).size !== numbers.length) return {status: "ambiguous", reason_codes: ["duplicate_revision"], points: [], ignored_future_records: ignoredFuture};
const sorted = [...numbers].sort((a, b) => a - b);
if (sorted.some((value, index) => value !== index)) return {status: "ambiguous", reason_codes: ["broken_revision_chain"], points: [], ignored_future_records: ignoredFuture};
const head = revisions.reduce((a, b) => (a.revision as number) > (b.revision as number) ? a : b);
if (head.event_type === "cancel") return {status: "incomplete", reason_codes: ["cancelled_session"], points: [], ignored_future_records: ignoredFuture};
if (head.source_evidence_state !== "ready") return {status: "incomplete", reason_codes: ["source_not_ready"], points: [], ignored_future_records: ignoredFuture};
const currentIdentity = IDENTITY_FIELDS.map((field) => head[field]);
if (currentIdentity.some((value) => typeof value !== "string" || !value)) throw new Error("series identity fields must be non-empty strings");
const encoded = JSON.stringify(currentIdentity);
if (identity === null) identity = encoded;
else if (identity !== encoded) return {status: "unsupported", reason_codes: ["series_identity_break"], points: [], ignored_future_records: ignoredFuture};
transformRecord(head);
resolved.push(head);
}
const points = calculateValues(resolved.map(transformRecord)).map((point, index) => ({...point, session_date: resolved[index].session_date}));
return {
status: "resolved", reason_codes: [], points,
latest_oscillator: points.at(-1)?.oscillator ?? null,
latest_index_value: points.at(-1)?.index_value ?? null,
ignored_future_records: ignoredFuture, seed_policy: "sma_19_39", initial_index: INITIAL_INDEX,
};
}
The embedded lab now expands to its full document height, keeping the article as the only scroll surface.