The governed definitions this build depends on. Read them first if a term is unfamiliar.
- PrimaryRatio-Adjusted Summation IndexA cumulative index that adds each ready Ratio-Adjusted McClellan Oscillator value to its prior level.
- PrerequisiteExponential Moving AverageAn Exponential Moving Average recursively gives recent observations greater influence than progressively older observations.
- PrerequisiteRatio-Adjusted BreadthRatio-adjusted breadth scales advances minus declines by advances plus declines.
- PrerequisiteRatio-Adjusted McClellan OscillatorA McClellan oscillator calculated from 1000 times net advances divided by advancing plus 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.
- ImportantCumulative Starting LevelThe declared initial constant from which a cumulative recurrence begins before accepted increments are added.
- ImportantCumulative StateA persisted running value updated by adding each newly accepted increment to the previous accumulated value.
- ImportantNo-Imputation Missing-Session PolicyA policy that blocks or pauses calculation instead of converting a missing required session into a zero observation.
- ImportantProcessing CheckpointA persisted valid state and provenance bundle from which deterministic processing can safely resume.
- 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.
- 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.
How can a cumulative breadth path remain comparable when the count of moving issues changes? Ratio-Adjusted Summation Index (RASI) 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 RASI when both normalization and cumulative state are required. RASI combines two decisions that must remain separate: normalize first, accumulate only after the oscillator is ready.
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 plus cumulative state |
| What happens when source scale doubles? | Doubling the moving-issue universe leaves the normalized oscillator and every RASI increment unchanged. |
| What is withheld? | A+D is zero, so the normalized input and cumulative update are both withheld. |
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.
The declared pre-output seed is 0. It translates the level; each daily increment still equals the oscillator.
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 | -58.3435 |
| 40 | stress and recovery | 333.3333 | 43.1017 | 82.4041 | -39.3023 | -97.6458 |
| 45 | stress and recovery | -266.6667 | 103.6062 | 112.1741 | -8.5679 | -76.5550 |
| 60 | stress and recovery | 55.5556 | 37.3116 | 58.1002 | -20.7886 | -507.3077 |
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 | -76.5550 |
| 2× | -266.6667 | -8.5679 | -76.5550 |
Doubling the moving-issue universe leaves the normalized oscillator and every RASI increment unchanged. 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 and cumulative update are both withheld. 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 Summation Index (RASI) 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 the moving-issue universe leaves the normalized oscillator and every RASI increment unchanged.
Next: McClellan Volume Oscillator.
Primary references
See the annotated source record and the canonical technical contract.
Rendered from the canonical Mermaid sources linked by this article.
Ratio-Adjusted Summation Index (RASI) calculation flow
Purpose: show that evidence, input units, recursive memory, and optional cumulative state are separate decisions.
Takeaway: Doubling the moving-issue universe leaves the normalized oscillator and every RASI increment unchanged.
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.
MCO-03 — The McClellan Oscillator and Summation Index
- Organization or authors: McClellan Financial Publications
- Source type: Maintained first-party overview
- Publication or effective date: Not stated unless shown on the source page
- Version: Web or PDF edition accessed 2026-07-25
- URL: The McClellan Oscillator and Summation Index
- Accessed: 2026-07-25
- Jurisdiction: United States market-breadth methodology
- Supports: The Summation Index as cumulative oscillator values and the historical +1000 convention for the classic issue-count series.
- Limitations: A published absolute level depends on calibration and source history; a fresh local seed does not reproduce a vendor series.
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 defines RASI by ratio-adjusting A-D breadth, applying the usual 10%/5% oscillator, and cumulatively adding the result with zero as the modern neutral convention.
- 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 Summation Index (RASI). */
export type RecordRow = Record<string, unknown>;
export const MODE = "ratio_issue_sum";
export const INITIAL_INDEX: number | null = 0.0;
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.