The governed definitions this build depends on. Read them first if a term is unfamiliar.
- PrimaryCumulative StateA persisted running value updated by adding each newly accepted increment to the previous accumulated value.
- PrerequisiteMarket BreadthMarket breadth describes how widely a market movement is shared across its eligible securities.
- PrerequisiteNet AdvancesNet Advances is the number of advancing issues minus the number of declining issues for one validated session.
- 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.
- ImportantNo-Imputation Missing-Session PolicyA policy that blocks or pauses calculation instead of converting a missing required session into a zero observation.
- 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.
- ImportantUniverse PartitionA universe partition assigns every eligible issue to exactly one declared classification bucket.
- MentionedProcessing CheckpointA persisted valid state and provenance bundle from which deterministic processing can safely resume.
A cumulative Advance/Decline Line looks like one addition per session. In production, the harder question is whether every addition belongs to the same series and was actually knowable at the requested time.
This tutorial builds a line that can answer that question. It declares its seed and calendar, keeps revisions in an append-only lineage, recomputes the dependent suffix after a correction, and publishes no main value when the evidence is incomplete, ambiguous, or incompatible.
Start with the recurrence
For session position t:
A_t and D_t are advancing and declining issue counts. s is an explicit seed. Fidelity describes the familiar A/D line as a cumulative total of advancing stocks less declining stocks. That definition does not choose your roster, calendar, seed, revision policy, or publication clock; those are system contracts.
Why state changes the engineering problem
Daily Net Advances is local. A cumulative point contains every earlier increment. If session k changes by delta:
then every point from k onward changes by the same delta:
What to notice: the revised branch does not reconnect at the next date. Every dependent point is rebuilt.
Declare one continuity segment
The implementation accepts a continuity contract with:
series_idandcontinuity_id;- venue, universe, calendar, and session identities;
- the prior-price comparison basis;
- a signed safe-integer seed and seed lineage;
- the seed's effective and available timestamps;
- an ordered list of expected session dates and contiguous positions.
If the universe method, calendar, session, comparison basis, or continuity identity changes, begin a new segment with an explicit seed. Do not splice the new record into the old line.
The seed sets the vertical origin:
What to notice: shifting the seed moves every level equally. It does not repair a gap or make incompatible series comparable.
Reconcile daily evidence before accumulating it
Every upsert supplies:
U_t is unchanged, X_t is excluded by a declared rule, Q_t is unclassified, and N_t is universe size. A source can claim source_evidence_state="ready" only when the partition reconciles and Q_t=0.
The daily evidence state is one of:
ready: eligible to enter a resolved cumulative line;incomplete: required evidence is missing;ambiguous: more than one interpretation remains;unsupported: the daily result cannot be represented by this contract.
is_final is separate. A structurally ready but provisional observation still makes the cumulative line incomplete because every later point depends on it.
Keep two clocks
Each revision records:
effective_at: when the information applies in market or business time;available_at: when this system received or accepted it.
At a knowledge_cutoff, only events with available_at <= knowledge_cutoff exist for the calculation. The implementation parses availability first and ignores future events before inspecting their identity, source state, or counts. A malformed future correction therefore cannot change an earlier answer.
This is the difference between reconstructing what was known and rebuilding history with hindsight.
Require one revision lineage
Visible events stay in supplied ingest order; the function never sorts them. Within each session:
- revision one has no parent;
- later revisions use the next contiguous number;
- each revision supersedes the exact previously selected event;
- a duplicate identity, skipped number, branch, or competing parent is
ambiguous; - the latest visible cancellation makes the expected session
incomplete.
Missing is not zero
The contract owns the expected session list. If a middle session is missing or cancelled, the algorithm stops the diagnostic prefix at that boundary. It does not insert zero and does not connect later observations across the gap.
An observed zero means advances equaled declines. A missing session means the increment is unknown. Drawing both as a flat step is misinformation.
Publish only resolved evidence
| Status | Interpretation | Main points | Final value |
|---|---|---|---|
resolved | Every expected session has one final, ready, reconciled revision. | Present | Present |
incomplete | Missing/cancelled session, provisional source, unavailable seed, or incomplete daily evidence. | Empty | Null |
ambiguous | Revision lineage or daily evidence cannot be selected uniquely. | Empty | Null |
unsupported | Metric, partition, calendar, daily state, or continuity identity is incompatible. | Empty | Null |
An incomplete result may include causal_prefix_diagnostic. The name is deliberate: it is troubleshooting evidence, not a partially publishable line.
Synthetic worked example
With seed zero:
| Session | Advances | Declines | Net | Cumulative |
|---|---|---|---|---|
| 2026-01-05 | 60 | 30 | +30 | 30 |
| 2026-01-06 | 35 | 55 | -20 | 10 |
| 2026-01-07 | 40 | 40 | 0 | 10 |
| 2026-01-08 | 65 | 25 | +40 | 50 |
| 2026-01-09 | 30 | 60 | -30 | 20 |
A synthetic second revision for January 6 changes Net Advances from -20 to -10. Before its availability, the original resolved line remains 30, 10, 10, 50, 20. After its availability, the rebuilt line is 30, 20, 20, 60, 30. The correction moves the entire suffix by +10.
Open the guided cumulative-line lab to step through resolved, corrected, missing-gap, provisional, series-break, and ambiguous-lineage scenarios. Step advances exactly one evidence transition. Reset restores the same initial state.
A bounded real observation, not a resolved package result
Nasdaq Trader provides yearly Daily Market Files and lists Nasdaq Advances, Declines, and Unchanged among the fields. The official 2026 CSV was independently checked on 2026-07-23.
| Date | Advances observed | Declines observed | Unchanged observed | Net derived | Zero-seed cumulative derived |
|---|---|---|---|---|---|
| 2026-03-02 | 2,443 | 2,431 | 228 | +12 | 12 |
| 2026-03-03 | 1,359 | 3,542 | 190 | -2,183 | -2,171 |
| 2026-03-04 | 3,368 | 1,509 | 224 | +1,859 | -312 |
| 2026-03-05 | 1,430 | 3,454 | 195 | -2,024 | -2,336 |
| 2026-03-06 | 1,461 | 3,384 | 238 | -1,923 | -4,259 |
The three count columns are provider observations. Net and cumulative columns are arithmetic derived here. The free file does not provide this package's stable point-in-time roster, excluded and unclassified partition, per-row availability timestamp, or revision chain. Therefore the table is not a resolved output, does not prove equal coverage across dates, and carries no causal, predictive, or trading claim.
Implementation and testing
The Python and TypeScript implementations share one synthetic fixture. Both reject safe-integer overflow and malformed timestamps, preserve input order, ignore future-only anomalies, recompute corrections, and suppress the main line for all non-resolved states.
The tests cover:
- canonical and translated-seed sequences;
- earlier and later correction cutoffs;
- missing, cancelled, and provisional sources;
- incomplete, ambiguous, and unsupported daily evidence;
- hidden unclassified issues;
- branches, duplicate IDs, and noncontiguous ingest positions;
- every stable identity break;
- calendar, partition, timestamp, overflow, and immutability failures;
- future-only series breaks, duplicate IDs, bad partitions, bad counts, and bad source states.
What this line does not prove
A rising segment means the included sessions added positive Net Advances overall; a falling segment means negative values accumulated. It does not identify why, predict the next return, or validate a strategy. Divergence detection requires separate peak rules, alignment, confirmation, and empirical testing. Fidelity itself cautions that A/D signals may not confirm trends or forecast reversals.
Correct recurrence and clean data contracts prove implementation fidelity. They do not prove profitability, causation, or investment suitability.
Summary
A trustworthy cumulative A/D line needs five disciplines:
- one explicit seed and continuity identity;
- one ordered calendar with no invented observations;
- complete daily issue classification;
- causal revision selection using availability time;
- full suffix recomputation and no publication outside
resolved.
The next topic, Normalized Advance/Decline Line, addresses scaling conventions. It does not relax any of these evidence requirements.
Rendered from the canonical Mermaid sources linked by this article.
Revision lineage and suffix recomputation
This diagram shows how a causally visible correction becomes a new resolved line.
Takeaway: a correction is never a local chart edit, and it cannot be applied before its availability time.
Causal recurrence and publication flow
This diagram separates causal filtering from publication.
Takeaway: future, missing, provisional, ambiguous, or incompatible evidence never becomes a publishable cumulative value.
References4 primary 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.
R1 — Nasdaq Trader Daily Market Files
- Organization: Nasdaq Trader
- Source type: Official exchange market-statistics download page
- Publication or effective date: Live year-to-date service
- Version: 2026 CSV checked 2026-07-23
- URL: Daily Market Files
- Data URL: 2026 CSV
- Accessed: 2026-07-23
- Jurisdiction: United States
- Supports: The existence and scope of downloadable yearly daily market-statistics files; the five bounded Advances, Declines, and Unchanged observations used in this package.
- Limitations: Does not supply this package's stable roster, excluded partition, per-row availability timestamp, or correction lineage. Redistribution terms were not inferred; only a small attributed observation is recorded.
R2 — Nasdaq Trader Daily Market Summary Definitions
- Organization: Nasdaq Trader
- Source type: Official exchange field definitions
- Publication or effective date: Live documentation
- Version: Reviewed 2026-07-23
- URL: Daily Market Summary Definitions
- Accessed: 2026-07-23
- Jurisdiction: United States
- Supports: The year-to-date file includes Nasdaq Advances, Declines, and Unchanged; issue counts are based on issues active for the provided date.
- Limitations: Does not define a cumulative recurrence, roster identity, exclusions, publication clock, or revisions.
R3 — Fidelity advance/decline education
- Organization: Fidelity Investments
- Source type: Maintained institutional education article
- Publication date: 2026-04-20
- Version: Live page reviewed 2026-07-23
- URL: Advance/decline indicator
- Accessed: 2026-07-23
- Jurisdiction: United States
- Supports: The familiar A/D line as the cumulative total of advancing stocks less declining stocks; caution that chart signals may not confirm or forecast reversals.
- Limitations: Educational interpretation, not an exchange methodology. Its current market commentary is not reused as evidence for this algorithm.
R4 — Nasdaq A-D glossary
- Organization: Nasdaq, Inc.
- Source type: Official exchange glossary
- Publication date: Not stated
- Version: Live page reviewed 2026-07-23
- URL: A-D definition
- Accessed: 2026-07-23
- Jurisdiction: United States
- Supports: Advances less declines relative to prior closing prices and the market-breadth context.
- Limitations: Does not specify the cumulative seed, universe, calendar, corrections, or software behavior.
Evidence boundary
The causal event model, two-clock semantics, safe-integer limits, revision-chain rules, explicit result states, and refusal to publish incomplete evidence are package design decisions. Correction examples are synthetic; no historical Nasdaq correction chain is claimed.
Full dependency-light reference implementations in both supported languages.
/** Causal, revision-aware Cumulative Advance/Decline Line. */
export type ResultStatus = "resolved" | "incomplete" | "ambiguous" | "unsupported";
export type Action = "upsert" | "cancel";
export interface ExpectedSession { session_sequence: number; session_date: string }
export interface SeriesContract {
metric: "cumulative_issue_count_advance_decline_line" | string;
series_id: string; continuity_id: string; venue_id: string; universe_id: string;
calendar_id: string; session_type: string; comparison_basis: string;
seed: number; seed_lineage_id: string; seed_effective_at: string;
seed_available_at: string; expected_sessions: ExpectedSession[];
}
export interface BreadthEvent {
event_id: string; ingest_sequence: number; revision_number: number;
supersedes_event_id: string | null; action: Action; session_sequence: number;
session_date: string; effective_at: string; available_at: string;
series_id: string; continuity_id: string; venue_id: string; universe_id: string;
calendar_id: string; session_type: string; comparison_basis: string;
universe_snapshot_id: string; is_final: boolean;
source_evidence_state: "ready" | "incomplete" | "ambiguous" | "unsupported";
advances?: number; declines?: number; unchanged?: number; excluded?: number; unclassified?: number;
universe_size?: number;
}
export interface BreadthLinePoint {
session_sequence: number; session_date: string; event_id: string;
revision_number: number; effective_at: string; available_at: string;
advances: number; declines: number; unchanged: number; excluded: number;
unclassified: number;
universe_size: number; net_advances: number; cumulative_line: number;
source_is_provisional: boolean; line_is_provisional: boolean;
universe_snapshot_id: string;
source_evidence_state: "ready";
}
export interface BreadthLineResult {
status: ResultStatus; reasons: string[]; knowledge_cutoff: string; seed: number;
points: BreadthLinePoint[]; missing_sessions: string[]; cancelled_sessions: string[];
final_value: number | null; causal_prefix_diagnostic: BreadthLinePoint[];
contains_provisional: boolean; ignored_future_event_count: number;
recompute_from_session: string | null; blocked_from_session: string | null;
metric: "cumulative_issue_count_advance_decline_line";
}
export class BreadthLineValidationError extends Error {
constructor(message: string) { super(message); this.name = "BreadthLineValidationError"; }
}
const MAX_SAFE = Number.MAX_SAFE_INTEGER;
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
const IDENTITY_FIELDS = ["series_id", "continuity_id", "venue_id", "universe_id", "calendar_id", "session_type", "comparison_basis"] as const;
const COUNT_FIELDS = ["advances", "declines", "unchanged", "excluded", "unclassified", "universe_size"] as const;
function text(value: unknown, field: string): string {
if (typeof value !== "string" || value.trim() === "") throw new BreadthLineValidationError(`${field} must be a non-empty string.`);
return value;
}
function timestamp(value: unknown, field: string): number {
if (typeof value !== "string" || !/(Z|[+-]\d{2}:\d{2})$/.test(value) || Number.isNaN(Date.parse(value))) throw new BreadthLineValidationError(`${field} must be an RFC 3339 timestamp with offset.`);
return Date.parse(value);
}
function date(value: unknown, field: string): string {
if (typeof value !== "string" || !DATE_RE.test(value) || new Date(`${value}T00:00:00Z`).toISOString().slice(0, 10) !== value) throw new BreadthLineValidationError(`${field} must be a real YYYY-MM-DD date.`);
return value;
}
function integer(value: unknown, field: string, nonnegative = false): number {
if (!Number.isSafeInteger(value) || (nonnegative && (value as number) < 0)) throw new BreadthLineValidationError(`${field} must be a ${nonnegative ? "non-negative " : ""}safe integer.`);
return value as number;
}
function base(status: ResultStatus, reasons: string[], cutoff: string, seed: number, ignored = 0): BreadthLineResult {
return {status, reasons, knowledge_cutoff: cutoff, seed, points: [], final_value: null, causal_prefix_diagnostic: [], missing_sessions: [], cancelled_sessions: [], contains_provisional: false, ignored_future_event_count: ignored, recompute_from_session: null, blocked_from_session: null, metric: "cumulative_issue_count_advance_decline_line"};
}
export function calculateCumulativeAdvanceDeclineLine(contract: SeriesContract, events: BreadthEvent[], knowledgeCutoff: string): BreadthLineResult {
if (contract === null || typeof contract !== "object") throw new BreadthLineValidationError("contract must be an object.");
if (!Array.isArray(events)) throw new BreadthLineValidationError("events must be an ordered array.");
const cutoff = timestamp(knowledgeCutoff, "knowledge_cutoff");
const seed = integer(contract.seed, "seed");
text(contract.seed_lineage_id, "seed_lineage_id");
const seedEffective = timestamp(contract.seed_effective_at, "seed_effective_at");
const seedAvailable = timestamp(contract.seed_available_at, "seed_available_at");
if (seedAvailable < seedEffective) throw new BreadthLineValidationError("seed_available_at cannot precede seed_effective_at.");
for (const field of IDENTITY_FIELDS) text(contract[field], field);
if (contract.metric !== "cumulative_issue_count_advance_decline_line") return base("unsupported", ["unsupported_metric"], knowledgeCutoff, seed);
if (!Array.isArray(contract.expected_sessions)) throw new BreadthLineValidationError("expected_sessions must be an ordered array.");
const expected: ExpectedSession[] = [];
let previousDate: string | null = null;
for (let i = 0; i < contract.expected_sessions.length; i++) {
const raw = contract.expected_sessions[i];
const sequence = integer(raw.session_sequence, "session_sequence", true);
const sessionDate = date(raw.session_date, "session_date");
if (sequence !== i + 1 || (previousDate !== null && sessionDate <= previousDate)) return base("unsupported", ["expected_session_sequence_not_contiguous"], knowledgeCutoff, seed);
expected.push({session_sequence: sequence, session_date: sessionDate}); previousDate = sessionDate;
}
const expectedByDate = new Map(expected.map(x => [x.session_date, x.session_sequence]));
if (cutoff < seedAvailable) { const result = base("incomplete", ["seed_not_yet_available"], knowledgeCutoff, seed); result.blocked_from_session = expected[0]?.session_date ?? null; return result; }
const validated: Array<BreadthEvent & {_available: number}> = [];
let previousAvailable: number | null = null;
const seen = new Set<string>(); let duplicate = false;
const unsupported = new Set<string>();
let ignored = 0;
for (let i = 0; i < events.length; i++) {
const raw = events[i]; if (raw === null || typeof raw !== "object") throw new BreadthLineValidationError("each event must be an object.");
const event = structuredClone(raw) as BreadthEvent & {_available: number};
const available = timestamp(event.available_at, "available_at");
if (available > cutoff) { ignored++; continue; }
const id = text(event.event_id, "event_id"); if (seen.has(id)) duplicate = true; seen.add(id);
if (integer(event.ingest_sequence, "ingest_sequence", true) !== validated.length + 1) unsupported.add("event_sequence_not_contiguous");
const effective = timestamp(event.effective_at, "effective_at");
if (available < effective) throw new BreadthLineValidationError("available_at cannot precede effective_at.");
if (previousAvailable !== null && available < previousAvailable) unsupported.add("events_not_in_ingest_order"); previousAvailable = available; event._available = available;
event.session_date = date(event.session_date, "session_date"); event.session_sequence = integer(event.session_sequence, "session_sequence", true); event.revision_number = integer(event.revision_number, "revision_number", true);
if (event.revision_number < 1) throw new BreadthLineValidationError("revision_number must be at least 1.");
if (event.action !== "upsert" && event.action !== "cancel") throw new BreadthLineValidationError("action must be upsert or cancel.");
if (event.supersedes_event_id !== null) text(event.supersedes_event_id, "supersedes_event_id");
for (const field of IDENTITY_FIELDS) { text(event[field], field); if (event[field] !== contract[field]) unsupported.add("series_break_required"); }
if (expectedByDate.get(event.session_date) !== event.session_sequence) unsupported.add("event_outside_expected_calendar");
text(event.universe_snapshot_id, "universe_snapshot_id"); if (typeof event.is_final !== "boolean") throw new BreadthLineValidationError("is_final must be a boolean.");
if (!["ready", "incomplete", "ambiguous", "unsupported"].includes(event.source_evidence_state)) throw new BreadthLineValidationError("source_evidence_state must be ready, incomplete, ambiguous, or unsupported.");
if (event.action === "upsert") {
const counts = Object.fromEntries(COUNT_FIELDS.map(f => [f, integer(event[f], f, true)])) as Record<typeof COUNT_FIELDS[number], number>;
if (counts.advances + counts.declines + counts.unchanged + counts.excluded + counts.unclassified !== counts.universe_size) unsupported.add("unreconciled_issue_partition");
if (event.source_evidence_state === "ready" && counts.unclassified !== 0) unsupported.add("ready_source_contains_unclassified_issues");
Object.assign(event, counts);
}
validated.push(event);
}
if (unsupported.size) return base("unsupported", [...unsupported].sort(), knowledgeCutoff, seed);
const visible = validated;
if (duplicate) return base("ambiguous", ["duplicate_event_id"], knowledgeCutoff, seed, ignored);
const grouped = new Map(expected.map(x => [x.session_date, [] as typeof visible]));
for (const event of visible) grouped.get(event.session_date)!.push(event);
const selected = new Map<string, typeof visible[number]>(); const corrected: string[] = [];
for (const session of expected) {
const chain = grouped.get(session.session_date)!; if (!chain.length) continue;
for (let i = 0; i < chain.length; i++) {
const requiredParent = i === 0 ? null : chain[i - 1].event_id;
if (chain[i].revision_number !== i + 1 || chain[i].supersedes_event_id !== requiredParent) return base("ambiguous", [`non_unique_revision_lineage:${session.session_date}`], knowledgeCutoff, seed, ignored);
}
selected.set(session.session_date, chain.at(-1)!); if (chain.length > 1) corrected.push(session.session_date);
}
let running = seed; let provisionalSeen = false; let blocked: string | null = null;
let sourceBlockStatus: ResultStatus | null = null;
const points: BreadthLinePoint[] = []; const missing: string[] = []; const cancelled: string[] = []; const reasons: string[] = [];
for (const session of expected) {
const event = selected.get(session.session_date);
if (!event) { missing.push(session.session_date); blocked ??= session.session_date; continue; }
if (event.action === "cancel") { cancelled.push(session.session_date); blocked ??= session.session_date; continue; }
const sourceState = event.source_evidence_state;
if (sourceState !== "ready") {
blocked ??= session.session_date; reasons.push(`source_evidence_${sourceState}:${session.session_date}`);
if (sourceState === "unsupported") sourceBlockStatus = "unsupported";
else if (sourceState === "ambiguous" && sourceBlockStatus !== "unsupported") sourceBlockStatus = "ambiguous";
else if (sourceBlockStatus === null) sourceBlockStatus = "incomplete";
continue;
}
if (blocked !== null) continue;
const advances = event.advances!; const declines = event.declines!; const net = advances - declines; running = integer(running + net, "cumulative_line");
const sourceProvisional = !event.is_final; provisionalSeen ||= sourceProvisional;
points.push({session_sequence: session.session_sequence, session_date: session.session_date, event_id: event.event_id, revision_number: event.revision_number, effective_at: event.effective_at, available_at: event.available_at, advances, declines, unchanged: event.unchanged!, excluded: event.excluded!, unclassified: event.unclassified!, universe_size: event.universe_size!, net_advances: net, cumulative_line: running, source_is_provisional: sourceProvisional, line_is_provisional: provisionalSeen, universe_snapshot_id: event.universe_snapshot_id, source_evidence_state: "ready"});
}
if (missing.length) reasons.push("missing_expected_session"); if (cancelled.length) reasons.push("cancelled_session"); if (provisionalSeen) reasons.push("provisional_source_in_suffix");
const status: ResultStatus = sourceBlockStatus ?? (reasons.length ? "incomplete" : "resolved");
const publishable = status === "resolved";
return {status, reasons, knowledge_cutoff: knowledgeCutoff, seed, points: publishable ? points : [], final_value: publishable ? (points.at(-1)?.cumulative_line ?? seed) : null, causal_prefix_diagnostic: publishable ? [] : points, missing_sessions: missing, cancelled_sessions: cancelled, contains_provisional: provisionalSeen, ignored_future_event_count: ignored, recompute_from_session: corrected.length ? [...corrected].sort()[0] : null, blocked_from_session: blocked, metric: "cumulative_issue_count_advance_decline_line"};
}
The embedded lab now expands to its full document height, keeping the article as the only scroll surface.