The governed definitions this build depends on. Read them first if a term is unfamiliar.
- PrimaryMoving IssuesMoving Issues is the sum of advancing and declining issue counts for a session.
- PrerequisiteAdvancing IssueAn advancing issue is an eligible security whose comparable price is above its prior comparable close.
- PrerequisiteDeclining IssueA declining issue is an eligible security whose comparable price is below its prior comparable close.
- PrerequisiteMarket BreadthMarket breadth describes how widely a market movement is shared across its eligible securities.
- PrerequisitePoint-in-Time Issue UniverseA point-in-time issue universe is the eligible security set established using information valid for the session being calculated.
- ImportantCausal Knowledge CutoffThe time boundary that prevents information unavailable at that moment from influencing a calculation.
- ImportantRevision LineageAn ordered chain connecting each correction to the exact earlier record it supersedes.
- ImportantUnchanged IssueAn unchanged issue is an eligible security whose comparable price did not move under the declared comparison policy.
- ImportantUniverse PartitionA universe partition assigns every eligible issue to exactly one declared classification bucket.
- ImportantZero-Mover SessionA zero-mover session has no advancing or declining issues, so A plus D equals zero.
- MentionedNet AdvancesNet Advances is the number of advancing issues minus the number of declining issues for one validated session.
A market index can move because a few heavily weighted securities moved. The Advance/Decline Ratio asks a different question: among the issues in one declared population, how many advanced for each issue that declined?
The answer is useful only when the denominator and the population are honest. A displayed value of 2 might represent 60 advances and 30 declines with complete coverage, or 6 advances and 3 declines while 90 issues are missing. Those observations share arithmetic, but not evidence quality.
This tutorial builds one exact variant: advancing issues divided by declining issues. It does not turn that ratio into a forecast or trading instruction.
The definition is narrow
For session (t):
- (A_t): issues with a comparable current close above the comparable prior close;
- (D_t): issues with a comparable current close below the comparable prior close.
Nasdaq's official glossary uses the same high-level definition: advancing stocks divided by declining stocks over a period. The glossary does not settle the production questions of universe membership, corporate-action adjustment, corrections, missing observations, or division by zero. Those belong in the implementation contract.
A ratio above 1 means advances outnumber declines. A ratio below 1 means declines outnumber advances. A ratio of exactly 1 means the two counts are equal.
Start with the issue universe
Before counting anything, declare:
- the venue or composite;
- the trading calendar and session;
- the point-in-time universe and membership revision;
- the allowed security types;
- a stable listing identifier, not only a reusable ticker;
- the current close and comparable prior-close basis;
- the corporate-action adjustment policy;
- the observation's effective time and the time it became available.
Every universe member must reconcile into one category:
| Category | Treatment |
|---|---|
| Advance | Enters the numerator |
| Decline | Enters the denominator |
| Unchanged | Classified, but enters neither side |
| New or no prior close | Excluded and reported |
| Halted | Excluded under the declared halt policy |
| Suspended | Excluded under the declared suspension policy |
| Delisted | Requires declared terminal/delisting treatment |
| Missing | Required observation absent |
| Unclassified | Present but not assignable under the policy |
The counts must sum exactly to the declared universe size. Missing count fields are errors; they are never interpreted as zeros.
Corporate actions matter because a raw prior close can be economically incomparable with today's close after a split or distribution. The ratio implementation receives already classified counts, so the data producer must preserve the issue-level evidence behind those counts.
Keep evidence state separate from ratio state
One status cannot explain both data quality and denominator behavior. This package returns two.
Evidence state:
resolved: final snapshot, full coverage, unique and complete correction chain;incomplete: provisional snapshot or one or more excluded issues;ambiguous: conflicting revisions, duplicate record IDs, a revision gap, or broken supersession;unsupported: no matching record was available at the requested as-of time.
Ratio state:
finite: declines are positive, so (A/D) is finite;no_declines: advances are positive and declines are zero;no_movers: both advances and declines are zero in a non-empty universe;empty_universe: the declared universe has no issues.
Only a resolved and finite observation receives a publishable ratio. An incomplete observation can show an observed_partition_ratio for diagnosis, but the main ratio remains null.
The picture makes the key separation visible: the denominator state answers whether division is defined; the evidence state answers whether the result is publishable.
Division by zero is not a formatting problem
Consider four boundary cases:
| Advances | Declines | Universe context | Result |
|---|---|---|---|
| 0 | 50 | Positive denominator | Finite ratio 0 |
| 50 | 0 | Advances only | no_declines, null ratio |
| 0 | 0 | All unchanged or excluded | no_movers, null ratio |
| 0 | 0 | Empty universe | empty_universe, null ratio and coverage |
Returning infinity for (A>0,D=0) is a possible display policy, but it is not a finite value and it does not serialize safely in ordinary JSON. Adding one to each count, capping the output, or carrying the previous ratio changes the algorithm. This package does none of those things silently.
A resolved synthetic calculation
Suppose a final, fully classified 100-issue universe contains:
60 advances
30 declines
10 unchanged
0 excluded
The partition reconciles to 100, and coverage is 100%. The ratio is:
The output is resolved, finite, and advances_dominant: two advancing issues for each declining issue.
Now change one unchanged issue into a new listing without a comparable prior close. The observed counts still yield 2, but coverage is 99%. The evidence state becomes incomplete; advance_decline_ratio is null, while observed_partition_ratio=2 remains available as a clearly labeled diagnostic.
The lesson is simple: a ratio without raw counts and coverage can conceal its weakest assumption.
Corrections require two clocks
effective_at says when a snapshot applies. available_at says when a researcher or live system could have known it.
For an as-of query, the algorithm:
- matches the exact venue, calendar, session, universe, listing scheme, security-type policy, comparison basis, and corporate-action policy;
- ignores records whose
available_atis later than the decision time; - requires unique record IDs and contiguous revisions starting at 1;
- verifies that every correction supersedes the immediately prior record;
- chooses the highest eligible revision.
If two eligible records claim the same revision, or the chain has a gap, the algorithm returns ambiguous rather than guessing. Future corrections do not leak into an earlier result.
A real exchange observation, with a strict boundary
Nasdaq Trader's official 2026 daily market-statistics CSV reports:
| Date | Advances | Declines | Unchanged | Author-derived (A/D) |
|---|---|---|---|---|
| 2026-03-05 | 1,430 | 3,454 | 195 | 0.4140127389 |
| 2026-06-26 | 3,107 | 1,912 | 192 | 1.625 |
The counts are source observations. The ratios are transparent divisions of those reported columns. This does not make them resolved package results.
The CSV does not provide the full point-in-time roster, stable listing IDs, excluded buckets, exact prior-close adjustment rules, publication timestamps, or a correction chain. For that reason, the two values are labeled observed-partition diagnostics, not authoritative complete-universe ratios.
To produce a resolved historical case, retain the security master, session calendar, issue-level current and prior comparable closes, corporate-action records, exclusion reasons, publication timestamps, and revision lineage. Official NYSE materials describe licensed trade/quote, closing-price, reference-data, and corporate-action products that could supply parts of that evidence. Product descriptions alone do not prove a historical ratio.
What this ratio is not
Do not substitute a nearby measure just because its name sounds similar:
- net advances are (A-D), not (A/D);
- percentage advancing uses a universe or classified-count denominator;
- normalized breadth is ((A-D)/(A+D));
- volume breadth weights by traded volume;
- capitalization breadth weights by market value;
- an index return aggregates price performance under index weights.
The Advance/Decline Ratio is an equal-count participation descriptor. A small and a large company each contribute one issue when both are eligible.
Implementation and validation
The Python and TypeScript implementations use one synthetic fixture set. Their tests cover:
- ratios above, below, and equal to one;
- zero advances, zero declines, all unchanged, and an empty universe;
- each exclusion category and incomplete coverage;
- provisional snapshots;
- missing, negative, fractional, and unsafe counts;
- conflicting, missing, duplicate, and broken revisions;
- policy mismatch and future-evidence isolation;
- separation from net advances and percentage advancing.
Passing these tests proves that the code follows the declared contract. It does not prove that the metric predicts returns or supports a profitable strategy.
Use the interactive lab to choose a scenario, step through evidence checks, and compare denominator and coverage states. Reset always returns to the same complete canonical preview.
Practical checklist
Before publishing a ratio, ask:
- Is the venue, session, calendar, and universe named?
- Are listing identities stable through ticker changes?
- Is the prior close comparable after corporate actions?
- Do all category counts reconcile to the universe?
- Are unchanged and every exclusion bucket visible?
- Is the correction available as of the evaluation time?
- Is the revision chain unique and complete?
- Is the evidence
resolvedand the ratio statefinite?
If any answer is no, show the diagnostic state instead of a confident-looking number.
Summary
The formula is advances divided by declines. The trustworthy result is larger than the formula: it includes stable universe identity, comparable closes, explicit exclusions, two clocks, correction lineage, coverage, and separate evidence and denominator states.
The next related topic is the Cumulative Advance/Decline Line, which accumulates net advances rather than this ratio.
Rendered from the canonical Mermaid sources linked by this article.
Evidence and ratio state flow
Takeaway: evidence quality and denominator behavior are separate state dimensions. A finite observed partition is publishable only when evidence is resolved.
Correction-aware snapshot lifecycle
Takeaway: effective_at describes the market state; available_at controls what was knowable for an as-of evaluation.
References7 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.
R01 - Advance/Decline Ratio glossary
- Organization: Nasdaq, Inc.
- Source type: Official exchange glossary
- Publication date: Undated live page
- URL: https://www.nasdaq.com/glossary/b/advance-decline-ratio
- Accessed: 2026-07-23
- Supports: The named ratio is advancing stocks divided by declining stocks over a period.
- Limitations: It does not define a production universe, corrections, coverage, or zero-denominator policy.
R02 - Daily Market Files landing page
- Organization: Nasdaq Trader
- Source type: Official exchange data page
- Publication date: Continuously maintained
- URL: https://www.nasdaqtrader.com/Trader.aspx?id=DailyMarketFiles
- Accessed: 2026-07-23
- Supports: Nasdaq publishes annual downloadable files containing daily market statistics.
- Limitations: The page and files do not supply this package's stable roster, exclusion partition, exact publication timestamp, or revision chain.
R03 - 2026 Daily Market Statistics CSV
- Organization: Nasdaq Trader
- Source type: Official exchange-hosted CSV
- Publication date: 2026 year-to-date file
- URL: https://www.nasdaqtrader.com/dynamic/dailyfiles/daily2026.csv
- Accessed: 2026-07-23
- Supports: Bounded observations used in the article: 2026-03-05 reports 1,430 advances, 3,454 declines, and 195 unchanged; 2026-06-26 reports 3,107 advances, 1,912 declines, and 192 unchanged.
- Limitations: The rows do not establish the complete point-in-time issue roster, excluded categories, stable listing identities, comparison-price adjustments, availability time, or correction lineage. Derived ratios are therefore observed-partition diagnostics, not
resolvedpackage outputs. Do not redistribute the source file without checking Nasdaq's terms.
R04 - Daily TAQ catalog
- Organization: New York Stock Exchange
- Source type: Official exchange product catalog
- Publication date: Live catalog
- URL: https://www.nyse.com/data-products/catalog/daily-taq
- Accessed: 2026-07-23
- Supports: A licensed reconstruction source can include previous-day trades and quotes, master data, and consolidated U.S. equity coverage; historical availability is described from 1993.
- Limitations: Product access and redistribution are licensed. The page does not publish the historical ratio used here.
R05 - TAQ NYSE Closing Prices Client Specification
- Organization: NYSE Technologies
- Source type: Official technical specification
- Version: 1.1, 2012-10-15
- URL: https://www.nyse.com/publicdocs/nyse/data/TAQ_NYSE_Closing_Prices_Client_Spec.pdf
- Accessed: 2026-07-23
- Supports: Closing-price files contain listed-issue identifiers and close-related fields and have documented daily delivery timing.
- Limitations: Older specification; not a current universal breadth methodology and not evidence for any numeric A/D row.
R06 - NYSE Reference Data
- Organization: New York Stock Exchange
- Source type: Official exchange product page
- Publication date: Live page
- URL: https://www.nyse.com/market-data/reference
- Accessed: 2026-07-23
- Supports: The NYSE Group Security Master is a daily exchange record for maintaining listed and traded security reference information.
- Limitations: Product description, not a free historical roster or an A/D calculation.
R07 - NYSE Corporate Actions
- Organization: New York Stock Exchange
- Source type: Official exchange product page
- Publication date: Live page
- URL: https://www.nyse.com/market-data/corporate-actions
- Accessed: 2026-07-23
- Supports: Corporate-action and listing-event data are separate inputs needed to make prior-close comparisons and membership treatment reproducible.
- Limitations: It does not prescribe this package's classification policy.
Evidence boundary
| Claim | Classification | Source |
|---|---|---|
| Ratio variant is advances divided by declines | Sourced definition | R01 |
| The two 2026 count triplets appear in an official Nasdaq-hosted CSV | Sourced observations | R02-R03 |
| 1,430 / 3,454 = 0.4140127389 and 3,107 / 1,912 = 1.625 | Derived arithmetic | Independently reproducible from R03 |
Those derived values are resolved historical ratios | Not claimed | Required audit fields are absent from R03 |
| Explicit states and null publishable ratio for incomplete evidence | Implementation choice | This package |
| The ratio predicts returns or produces a trade | Not claimed | Requires separate empirical research |
Full dependency-light reference implementations in both supported languages.
/** Correction-aware reference implementation for advancing issues / declining issues. */
export type EvidenceState = "resolved" | "incomplete" | "ambiguous" | "unsupported";
export type RatioState = "finite" | "no_declines" | "no_movers" | "empty_universe";
export type Direction = "advances_dominant" | "declines_dominant" | "balanced" | "advances_only" | "no_movers" | "no_universe";
export interface BreadthSnapshot {
series_id: string; record_id: string; session_date: string;
effective_at: string; available_at: string; venue_id: string;
calendar_id: string; session_id: string; universe_id: string;
universe_revision: string; listing_id_scheme: string;
security_type_policy: string; comparison_basis: string;
corporate_action_policy: string; revision: number; is_final: boolean;
supersedes_record_id: string | null;
advances: number; declines: number; unchanged: number;
new_or_no_prior_close: number; halted: number; suspended: number;
delisted: number; missing: number; unclassified: number; universe_size: number;
}
export interface BreadthQuery {
session_date: string; decision_as_of: string; venue_id: string;
universe_id: string; series_id: string;
calendar_id: string; session_id: string; universe_revision: string;
listing_id_scheme: string; security_type_policy: string;
comparison_basis: string; corporate_action_policy: string;
}
export interface BreadthResult {
evidence_state: EvidenceState; ratio_state: RatioState | null;
advance_decline_ratio: number | null; direction: Direction | null;
observed_partition_ratio: number | null; observed_partition_direction: Direction | null;
selected_record_id: string | null; selected_revision: number | null;
advances: number | null; declines: number | null; unchanged: number | null;
excluded_count: number | null; universe_size: number | null;
mover_count: number | null; classified_count: number | null;
coverage_ratio: number | null; is_provisional: boolean | null;
metric: "advancing_issues_divided_by_declining_issues"; reasons: string[];
}
export class BreadthValidationError extends Error {
constructor(message: string) { super(message); this.name = "BreadthValidationError"; }
}
const TEXT_FIELDS = ["series_id", "record_id", "venue_id", "calendar_id", "session_id", "universe_id", "universe_revision", "listing_id_scheme", "security_type_policy", "comparison_basis", "corporate_action_policy"] as const;
const COUNT_FIELDS = ["advances", "declines", "unchanged", "new_or_no_prior_close", "halted", "suspended", "delisted", "missing", "unclassified", "universe_size"] as const;
const EXCLUSION_FIELDS = ["new_or_no_prior_close", "halted", "suspended", "delisted", "missing", "unclassified"] as const;
const QUERY_POLICY_FIELDS = ["calendar_id", "session_id", "universe_revision", "listing_id_scheme", "security_type_policy", "comparison_basis", "corporate_action_policy"] as const;
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
const RFC3339_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
function validDate(value: unknown, field: string): asserts value is string {
if (typeof value !== "string" || !DATE_RE.test(value)) throw new BreadthValidationError(`${field} must be a valid YYYY-MM-DD date.`);
const [y, m, d] = value.split("-").map(Number); const parsed = new Date(Date.UTC(y, m - 1, d));
if (parsed.getUTCFullYear() !== y || parsed.getUTCMonth() !== m - 1 || parsed.getUTCDate() !== d) throw new BreadthValidationError(`${field} must be a valid YYYY-MM-DD date.`);
}
function validTimestamp(value: unknown, field: string): number {
if (typeof value !== "string" || !RFC3339_RE.test(value) || Number.isNaN(Date.parse(value))) throw new BreadthValidationError(`${field} must be an RFC 3339 timestamp.`);
return Date.parse(value);
}
function validCount(value: unknown, field: string): number {
if (!Number.isSafeInteger(value) || (value as number) < 0) throw new BreadthValidationError(`${field} must be a non-negative cross-language safe integer.`);
return value as number;
}
function validateSnapshot(snapshot: BreadthSnapshot): BreadthSnapshot {
if (snapshot === null || typeof snapshot !== "object") throw new BreadthValidationError("snapshot must be an object.");
validDate(snapshot.session_date, "session_date");
const effective = validTimestamp(snapshot.effective_at, "effective_at");
const available = validTimestamp(snapshot.available_at, "available_at");
if (available < effective) throw new BreadthValidationError("available_at cannot precede effective_at.");
for (const field of TEXT_FIELDS) if (typeof snapshot[field] !== "string" || snapshot[field].trim() === "") throw new BreadthValidationError(`${field} must be a non-empty string.`);
if (snapshot.supersedes_record_id !== null && (typeof snapshot.supersedes_record_id !== "string" || snapshot.supersedes_record_id.trim() === "")) throw new BreadthValidationError("supersedes_record_id must be null or a non-empty string.");
if (validCount(snapshot.revision, "revision") < 1) throw new BreadthValidationError("revision must be at least 1.");
if (typeof snapshot.is_final !== "boolean") throw new BreadthValidationError("is_final must be a boolean.");
const counts = Object.fromEntries(COUNT_FIELDS.map((f) => [f, validCount(snapshot[f], f)])) as Record<(typeof COUNT_FIELDS)[number], number>;
const partition = COUNT_FIELDS.filter((f) => f !== "universe_size").reduce((sum, f) => sum + counts[f], 0);
if (partition !== counts.universe_size) throw new BreadthValidationError("all classification buckets must sum exactly to universe_size.");
return {...snapshot, ...counts};
}
export function calculateAdvanceDeclineRatio(snapshot: BreadthSnapshot): BreadthResult {
const item = validateSnapshot(snapshot); const a = item.advances; const d = item.declines;
const excluded = EXCLUSION_FIELDS.reduce((sum, f) => sum + item[f], 0);
const movers = a + d; const classified = movers + item.unchanged;
const coverage = item.universe_size === 0 ? null : classified / item.universe_size;
let ratioState: RatioState; let ratio: number | null; let direction: Direction;
if (item.universe_size === 0) { ratioState = "empty_universe"; ratio = null; direction = "no_universe"; }
else if (movers === 0) { ratioState = "no_movers"; ratio = null; direction = "no_movers"; }
else if (d === 0) { ratioState = "no_declines"; ratio = null; direction = "advances_only"; }
else { ratioState = "finite"; ratio = a / d; direction = ratio > 1 ? "advances_dominant" : ratio < 1 ? "declines_dominant" : "balanced"; }
const reasons: string[] = []; if (!item.is_final) reasons.push("snapshot_is_provisional"); if (excluded > 0) reasons.push("coverage_is_incomplete");
const evidenceState: EvidenceState = reasons.length ? "incomplete" : "resolved";
return {evidence_state: evidenceState, ratio_state: ratioState, advance_decline_ratio: evidenceState === "resolved" ? ratio : null,
direction: evidenceState === "resolved" ? direction : null, observed_partition_ratio: ratio, observed_partition_direction: direction,
selected_record_id: item.record_id, selected_revision: item.revision, advances: a, declines: d, unchanged: item.unchanged,
excluded_count: excluded, universe_size: item.universe_size, mover_count: movers, classified_count: classified,
coverage_ratio: coverage, is_provisional: !item.is_final, metric: "advancing_issues_divided_by_declining_issues", reasons};
}
function emptyResult(state: EvidenceState, reason: string): BreadthResult {
return {evidence_state: state, ratio_state: null, advance_decline_ratio: null, direction: null, observed_partition_ratio: null, observed_partition_direction: null, selected_record_id: null, selected_revision: null,
advances: null, declines: null, unchanged: null, excluded_count: null, universe_size: null, mover_count: null, classified_count: null,
coverage_ratio: null, is_provisional: null, metric: "advancing_issues_divided_by_declining_issues", reasons: [reason]};
}
export function evaluateAdvanceDeclineRatioAsOf(snapshots: BreadthSnapshot[], query: BreadthQuery): BreadthResult {
if (query === null || typeof query !== "object") throw new BreadthValidationError("query must be an object.");
validDate(query.session_date, "query.session_date"); const decision = validTimestamp(query.decision_as_of, "query.decision_as_of");
for (const field of ["venue_id", "universe_id", "series_id", ...QUERY_POLICY_FIELDS] as const) if (typeof query[field] !== "string" || query[field].trim() === "") throw new BreadthValidationError(`query.${field} must be a non-empty string.`);
const eligible = snapshots.filter((raw) => {
if (raw === null || typeof raw !== "object") throw new BreadthValidationError("each snapshot must be an object.");
if (raw.session_date !== query.session_date || raw.venue_id !== query.venue_id || raw.universe_id !== query.universe_id || raw.series_id !== query.series_id || QUERY_POLICY_FIELDS.some((field) => raw[field] !== query[field])) return false;
return validTimestamp(raw.available_at, "available_at") <= decision;
}).map(validateSnapshot);
if (!eligible.length) return emptyResult("unsupported", "no_snapshot_available_as_of_query");
if (new Set(eligible.map((item) => item.record_id)).size !== eligible.length) return emptyResult("ambiguous", "duplicate_record_id");
const highest = Math.max(...eligible.map((item) => item.revision));
const byRevision = new Map<number, BreadthSnapshot[]>(); for (const item of eligible) byRevision.set(item.revision, [...(byRevision.get(item.revision) ?? []), item]);
for (let revision = 1; revision <= highest; revision += 1) if (!byRevision.has(revision)) return emptyResult("ambiguous", "non_contiguous_revision_chain");
if ([...byRevision.values()].some((items) => items.length !== 1)) return emptyResult("ambiguous", "conflicting_highest_revision");
for (let revision = 1; revision <= highest; revision += 1) { const current = byRevision.get(revision)![0]; const expected = revision === 1 ? null : byRevision.get(revision - 1)![0].record_id; if (current.supersedes_record_id !== expected) return emptyResult("ambiguous", "broken_supersession_chain"); }
return calculateAdvanceDeclineRatio(byRevision.get(highest)![0]);
}
The embedded lab now expands to its full document height, keeping the article as the only scroll surface.