D02-F01-A05 / Complete engineering topic

CRSP Cumulative Share/Volume Adjustment

A production-minded guide to CRSP Cumulative Share/Volume Adjustment.

D02 · CORPORATE ACTIONS AND SECURIT…
D02-F01-A05Canonical / Tested / Open
D02 / D02-F01
Key concepts

The governed definitions this build depends on. Read them first if a term is unfamiliar.

CRSP's cumulative share/volume factors can put historical shares outstanding and traded volume on one CRSP-defined share basis. The valuable result is not merely a smoother series. It is a result whose CRSP field, release, basis date, missing state, and limitations remain auditable.

This article is specifically about CRSP US Stock & Indexes Flat File Format 2.0 (CIZ). It does not present CRSP's convention as universal corporate-action truth.

What CRSP defines

The current CRSP CIZ 2.0 database guide defines DlyCumFacShr and MthCumFacShr as cumulative factors for shares and volume arising from split-type events. CRSP sets each series to 1 on the security's last trading day. For a security that delists from tracked exchanges and later returns, CRSP continues the factor across the gap as if no split occurred during that gap.

Those last two sentences are vendor rules. A carried factor does not prove that nothing happened to the issuer while CRSP did not track the security.

CRSP factor direction and basis

Use the exact CRSP share/volume field

For daily records, use DlyCumFacShr. For monthly records, use MthCumFacShr. Do not substitute DlyCumFacPr or MthCumFacPr. CRSP's own documentation distinguishes events where price and share factors differ.

The package calculation is:

QtCRSP=round12 ⁣(QtrawFtCRSP)Q^{\mathrm{CRSP}}_t = \operatorname{round}_{12}\!\left(Q^{\mathrm{raw}}_t F^{\mathrm{CRSP}}_t\right)

Here QtrawQ^{\mathrm{raw}}_t is a raw CRSP shares-outstanding or volume field, and FtCRSPF^{\mathrm{CRSP}}_t is the same-date, same-security-lineage, same-release cumulative share factor. Multiplication is the direction. The result is named CRSP-adjusted because its basis comes from CRSP's factor series. The public fixture uses SYNTHETIC-CRSP-CIZ-001, not a fabricated PERMNO; production lineage must retain the real CRSP-assigned PERMNO from the licensed source.

The CRSP release belongs in the result. CRSP's June 2025 release notes changed cumulative-factor display precision from 6.4 to 24.12. This implementation makes a separate package choice: round once at its public output to 12 decimal places using half-away-from-zero, then report a reverse residual. Dividing the rounded result by the factor may not reproduce the raw value exactly.

A synthetic four-for-one example

Suppose a synthetic CRSP-shaped daily series has this state:

DateRaw shares (000s)Raw volumeDlyCumFacShrCRSP-basis shares (000s)CRSP-basis volume
2024-08-2825010041,000400
2024-08-2925012541,000500
2024-08-301,00046011,000460

The first calculation is 250×4=1,000250 \times 4 = 1{,}000. The second quantity uses the same CRSP share/volume factor: 125×4=500125 \times 4 = 500.

Synthetic CRSP-shaped worked example

Every date, package key, quantity, and event in this table is synthetic. The package key is not a PERMNO. The table demonstrates direction; it does not claim a historical CRSP observation for a real company.

Daily and monthly are not interchangeable

Daily volume is a daily raw count, so each available DlyVol can be paired with its same-date DlyCumFacShr.

Monthly volume is an aggregate. CRSP CIZ 2.0 defines MthFacShrFlg codes N (none), S (stock split or dividend), O (other), and B (both). If the share factor changes inside the month, one month-end multiplier cannot describe the different bases of all component days. The package calculates the small monthly path only for N; it blocks S, O, B, and explicit null.

The safe package derivation is to return to licensed daily CRSP records:

VmCRSP=dmVdrawDlyCumFacShrdV^{\mathrm{CRSP}}_m = \sum_{d \in m} V^{\mathrm{raw}}_d DlyCumFacShr_d

This daily reconstruction is not implemented in the small function because it requires the full daily licensed data. Month-end shares are a separate endpoint quantity and may still be adjusted with MthCumFacShr.

Daily calculation versus blocked mixed-basis month

Missing values, gaps, and partial history

The package uses explicit nulls and statuses:

  • missing raw quantity becomes missing_raw;
  • missing CRSP factor becomes missing_crsp_factor;
  • a factor continued across a CRSP delisting/relisting gap becomes computed_crsp_gap_assumption;
  • a monthly aggregate with MthFacShrFlg S, O, B, or null becomes blocked_mixed_basis_month.

An adapter must still decode the actual CRSP delivery's nullable fields and other flags. Negative legacy sentinels are rejected rather than treated as quantities. The package preserves literal MthFacShrFlg and emits a separate normalized diagnostic.

A partial local extract does not create a new factor basis. Never force the last local row to 1. Keep the CRSP basis date from the factor delivery, even when the local analysis starts late or ends early.

CRSP's May 2026 release notes provide a concrete operational lesson: monthly cumulative-factor rows for incomplete terminal months of delisted securities had been omitted and were added back. That public correction supports preserving release vintage and coverage diagnostics. It does not supply a redistributable issuer-level factor example.

Calculation flow

Rendering system map…

The important separation is between source evidence and package interpretation. CRSP supplies the field and its vendor basis. The package validates, calculates, and labels. It does not independently validate the issuer event from another data provider.

Explore the scenarios

Open the guided playground. The canonical scenario steps through 24 synthetic daily observations and a share-basis transition. The monthly scenario shows why aggregate volume is blocked. The gap scenario makes CRSP's continuation assumption visible, and the missing-factor scenario stops instead of imputing.

Back, Step, Play, Pause, Reset, and speed all use the same deterministic transition. Keyboard controls work without pointer input, and reduced-motion mode disables autoplay while preserving stepwise learning.

Implementation and tests

The Python implementation and TypeScript implementation consume the same synthetic fixture. They validate CIZ 2.0, factor field, one unmistakable package key, sorted dates, one non-mixed frequency, CRSP basis date, missing states, and monthly aggregate evidence before multiplying. A tie regression pins the shared half-away-from-zero rule. A production adapter remains responsible for preserving the real licensed CRSP PERMNO.

Tests prove definition-level parity for this package contract. They do not prove that a subscriber's CRSP extraction, entitlement, field mapping, or release archive is correct.

Historical-evidence decision

This article does not publish a real company's CRSP factor path. Public CRSP manuals explain the fields, but the needed security observations are licensed and redistribution rights were not established. Synthetic values are more honest than an unattributed real-looking table.

The only real historical item used is CRSP's public 2026 release correction about incomplete delisting-month rows. It explains why release lineage matters; it is not a factual claim about any issuer's split or volume.

Summary

Use CRSP's share/volume factor, not its price factor. Multiply in the documented direction, preserve CRSP's terminal basis and release, distinguish daily observations from monthly aggregates, and keep missing, gap, delisting, and partial-history states visible. A CRSP-adjusted result is valuable precisely because it says which vendor convention produced it.

Continue with Rights-Issue TERP Adjustment to see an event where price and share treatment must remain separate.

Asset map

AssetLearning purposeSourceStatus
CRSP terminal basisDirection and vendor scopevisuals/static/article-hero.svgReady
Synthetic arithmeticVerify multiplicationvisuals/static/worked-example.svgReady
Monthly guardExplain daily reconstructionvisuals/static/failure-guard.svgReady
Guided labCompare canonical, monthly, gap, and missing statesvisuals/animated/playground.htmlReady

Source roles and limitations are recorded in ../REFERENCES.md.

CRSP CIZ 2.0 calculation flow

This flow separates a sourced CRSP field from package validation, calculation, and interpretation.

Rendering system map…

Takeaway: the package calculates a CRSP-scoped result only after verifying the exact CRSP share/volume factor field.

CRSP evidence-state lifecycle

The lifecycle keeps a CRSP result from looking more certain than its CRSP source state.

Rendering system map…

Takeaway: computed, gap-qualified, missing, rejected, and reconstruction-required are different CRSP-scoped outcomes.

References5 primary sources and evidence notes

Expand the source trail, evidence role, and limitations behind the engineering choices.

All sources were accessed 2026-07-22. Only CRSP/Morningstar primary documentation is used for CRSP field claims. No vendor observations are redistributed.

R1 - CRSP US Stock & Indexes Database Guide, Flat File Format 2.0 (CIZ)

  • Organization: Center for Research in Security Prices (CRSP), now surfaced by Morningstar Indexes
  • Source type: Official vendor schema and field-definition guide
  • Version: Flat File Format 2.0 (CIZ), Document last modified April 27, 2026
  • URL: https://www.crsp.org/crsp_pdf/crsp-us-stock-indexes-databases-guide-flat-file-format-2-0/
  • Jurisdiction/product: CRSP US Stock & Indexes
  • Supports: DlyShrOut, DlyVol, DlyCumFacShr, MthShrOut, MthCumFacShr, literal MthFacShrFlg codes N/S/O/B, PERMNO, terminal basis of 1, and CRSP's delisting/relisting-gap continuation convention
  • Limitations: Documents CRSP's licensed product convention. It does not make the convention universal and does not grant redistribution rights for security observations.

R2 - Important Notice: CRSP US Stock & Indexes Flat File Format 2.0 (CIZ)

R3 - June 2025 Monthly Update: CRSP US Stock & Index Release Notes

  • Organization: CRSP
  • Source type: Official product release notes
  • Publication/effective context: June 2025 release, data ending 2025-06-30
  • URL: https://www.crsp.org/wp-content/uploads/mdaz_202506.pdf
  • Supports: Cumulative adjustment factor fields DlyCumFacPr, DlyCumFacShr, MthCumFacPr, and MthCumFacShr moved from 6.4 to 24.12 display precision
  • Limitations: Describes representation precision; it does not imply that binary floating-point arithmetic is exact or that older extracts are wrong

R4 - May 2026 Monthly Update: CRSP US Stock & Index Databases Release Notes

  • Organization: CRSP
  • Source type: Official product release notes
  • Publication/effective context: May 2026 release, data ending 2026-04-30
  • URL: https://www.crsp.org/wp-content/uploads/mdaz_202604.pdf
  • Supports: CRSP corrections to cumulative adjustment factor file formatting and restoration of monthly cumulative-factor records for incomplete months at the end of delisted security histories
  • Limitations: The note establishes an operational revision, not a named issuer's adjustment factor or a universal delisting rule

R5 - CRSP US Stock & Indexes Database Data Descriptions Guide

  • Organization: CRSP
  • Source type: Official legacy CRSPAccess data dictionary and calculation guide
  • Version: Current archived guide accessed 2026-07-22
  • URL: https://www.crsp.org/wp-content/uploads/guides/CRSP_US_Stock_%26_Indexes_Database_Data_Descriptions_Guide.pdf
  • Supports: Historical CRSPAccess cumfacshr/mcumfacshr terminology, shares-outstanding units, raw volume characteristics, legacy missing-value behavior, and the warning that price and share factors can differ
  • Limitations: CRSPAccess is retired. The implementation does not accept this legacy schema; adapters must translate source-specific sentinels and fields into the explicit CIZ 2.0 package contract.
crsp_cumulative_share_volume_adjustment.ts
/** CRSP CIZ 2.0 cumulative share/volume adjustment reference.
 *
 * This is the documented CRSP vendor convention. A CRSP price factor is not
 * accepted as a substitute for DlyCumFacShr or MthCumFacShr.
 */

export const SCHEMA_VERSION = "CRSP_US_STOCK_CIZ_2_0" as const;
export const ROUNDING_DIGITS = 12;

type Frequency = "daily" | "monthly";
type FactorStatus = "observed" | "continued_across_gap" | "missing";
type MonthlyFlagCode = "N" | "S" | "O" | "B";
type MonthlyFlagState = "none" | "within_period" | "unknown";

export interface CrspInputRecord {
  packageSecurityKey: string;
  date: string;
  frequency: Frequency;
  crspFactorField: "DlyCumFacShr" | "MthCumFacShr";
  crspCumulativeShareFactor: number | null;
  crspFactorStatus: FactorStatus;
  rawSharesOutstandingThousands: number | null;
  rawVolumeShares: number | null;
  crspMthFacShrFlg?: MonthlyFlagCode | null;
}

export interface CrspAdjustmentInput {
  schemaVersion: typeof SCHEMA_VERSION;
  sourceRelease: string;
  crspBasisDate: string;
  extractCoverage: "full_history" | "partial_extract";
  records: CrspInputRecord[];
}

const factorFields: Record<Frequency, "DlyCumFacShr" | "MthCumFacShr"> = {
  daily: "DlyCumFacShr",
  monthly: "MthCumFacShr",
};
const factorStatuses = new Set<FactorStatus>(["observed", "continued_across_gap", "missing"]);
const monthlyFlagCodes = new Set<MonthlyFlagCode>(["N", "S", "O", "B"]);

function isoDate(value: unknown, name: string): string {
  if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(value)) {
    throw new TypeError(`${name} must be YYYY-MM-DD`);
  }
  const parsed = new Date(`${value}T00:00:00Z`);
  if (Number.isNaN(parsed.valueOf()) || parsed.toISOString().slice(0, 10) !== value) {
    throw new RangeError(`${name} must be YYYY-MM-DD`);
  }
  return value;
}

function numberOrNull(value: unknown, name: string, positive = false): number | null {
  if (value === null) return null;
  if (typeof value !== "number" || !Number.isFinite(value)) {
    throw new TypeError(`${name} must be finite or null`);
  }
  if (positive ? value <= 0 : value < 0) {
    throw new RangeError(`${name} must be ${positive ? "positive" : "non-negative"}`);
  }
  return value;
}

function rounded(value: number): number {
  if (!Number.isFinite(value)) throw new RangeError("value must be finite before rounding");
  const negative = value < 0;
  const [mantissa, exponentText = "0"] = Math.abs(value).toString().toLowerCase().split("e");
  const [whole, fraction = ""] = mantissa.split(".");
  const digits = BigInt(`${whole}${fraction}`);
  const decimalExponent = Number(exponentText) - fraction.length;
  const scaledExponent = decimalExponent + ROUNDING_DIGITS;
  let scaledInteger: bigint;
  if (scaledExponent >= 0) {
    scaledInteger = digits * (10n ** BigInt(scaledExponent));
  } else {
    const divisor = 10n ** BigInt(-scaledExponent);
    const quotient = digits / divisor;
    const remainder = digits % divisor;
    scaledInteger = quotient + (remainder * 2n >= divisor ? 1n : 0n);
  }
  if (scaledInteger === 0n) return 0;
  const padded = scaledInteger.toString().padStart(ROUNDING_DIGITS + 1, "0");
  const integerPart = padded.slice(0, -ROUNDING_DIGITS);
  const fractionalPart = padded.slice(-ROUNDING_DIGITS).replace(/0+$/, "");
  const decimalText = fractionalPart ? `${integerPart}.${fractionalPart}` : integerPart;
  return Number(`${negative ? "-" : ""}${decimalText}`);
}

function quantityResult(
  raw: number | null,
  factor: number | null,
  factorStatus: FactorStatus,
  monthlyVolumeFlag?: MonthlyFlagState,
): [string, number | null, number | null] {
  if (raw === null) return ["missing_raw", null, null];
  if (factorStatus === "missing") return ["missing_crsp_factor", null, null];
  if (monthlyVolumeFlag === "within_period" || monthlyVolumeFlag === "unknown") {
    return ["blocked_mixed_basis_month", null, null];
  }
  if (factor === null) throw new Error("internal factor validation failure");
  const adjusted = rounded(raw * factor);
  const residual = rounded(adjusted / factor - raw);
  const status = factorStatus === "continued_across_gap"
    ? "computed_crsp_gap_assumption"
    : "computed_crsp";
  return [status, adjusted, residual];
}

export function calculate(data: CrspAdjustmentInput) {
  if (!data || typeof data !== "object" || Array.isArray(data)) {
    throw new TypeError("data must be an object");
  }
  if (data.schemaVersion !== SCHEMA_VERSION) {
    throw new RangeError(`schemaVersion must equal ${SCHEMA_VERSION}`);
  }
  if (typeof data.sourceRelease !== "string" || !data.sourceRelease.trim()) {
    throw new TypeError("sourceRelease must be a non-empty string");
  }
  const basisDate = isoDate(data.crspBasisDate, "crspBasisDate");
  if (data.extractCoverage !== "full_history" && data.extractCoverage !== "partial_extract") {
    throw new RangeError("extractCoverage must be full_history or partial_extract");
  }
  if (!Array.isArray(data.records) || data.records.length === 0) {
    throw new TypeError("records must be a non-empty list");
  }

  let expectedSecurityKey: string | null = null;
  let expectedFrequency: Frequency | null = null;
  let previousDate: string | null = null;
  let lastFactor: number | null = null;
  let lastFactorStatus: FactorStatus | null = null;
  const rows = data.records.map((record, index) => {
    if (!record || typeof record !== "object" || Array.isArray(record)) {
      throw new TypeError(`records[${index}] must be an object`);
    }
    if (typeof record.packageSecurityKey !== "string" || !record.packageSecurityKey.trim()) {
      throw new TypeError(`records[${index}].packageSecurityKey must be a non-empty string`);
    }
    const securityKey = record.packageSecurityKey.trim();
    if (!securityKey.startsWith("SYNTHETIC-")) {
      throw new RangeError("packageSecurityKey must begin SYNTHETIC-; real CRSP PERMNO belongs in licensed lineage");
    }
    expectedSecurityKey ??= securityKey;
    if (securityKey !== expectedSecurityKey) {
      throw new RangeError("one calculation may contain only one packageSecurityKey");
    }
    const recordDate = isoDate(record.date, `records[${index}].date`);
    if (recordDate > basisDate) throw new RangeError("record date cannot be after the CRSP basis date");
    if (previousDate !== null && recordDate <= previousDate) {
      throw new RangeError("record dates must be strictly increasing");
    }
    previousDate = recordDate;
    if (record.frequency !== "daily" && record.frequency !== "monthly") {
      throw new RangeError("frequency must be daily or monthly");
    }
    expectedFrequency ??= record.frequency;
    if (record.frequency !== expectedFrequency) {
      throw new RangeError("one calculation cannot mix daily and monthly CRSP frequencies");
    }
    const expectedField = factorFields[record.frequency];
    if (record.crspFactorField !== expectedField) {
      throw new RangeError(`${record.frequency} records require CRSP field ${expectedField}`);
    }
    if (!factorStatuses.has(record.crspFactorStatus)) throw new RangeError("invalid crspFactorStatus");
    const factor = numberOrNull(record.crspCumulativeShareFactor, "crspCumulativeShareFactor", true);
    if (record.crspFactorStatus === "missing" && factor !== null) {
      throw new RangeError("a missing CRSP factor must have a null value");
    }
    if (record.crspFactorStatus !== "missing" && factor === null) {
      throw new RangeError("an observed CRSP factor must have a value");
    }
    lastFactor = factor;
    lastFactorStatus = record.crspFactorStatus;
    const shares = numberOrNull(record.rawSharesOutstandingThousands, "rawSharesOutstandingThousands");
    const volume = numberOrNull(record.rawVolumeShares, "rawVolumeShares");
    if (shares === null && volume === null) {
      throw new RangeError("each row needs a raw shares or raw volume value");
    }
    const monthlyCode = record.crspMthFacShrFlg;
    let monthlyState: MonthlyFlagState | undefined;
    if (record.frequency === "monthly") {
      if (monthlyCode === undefined || (monthlyCode !== null && !monthlyFlagCodes.has(monthlyCode))) {
        throw new RangeError("monthly rows require CRSP MthFacShrFlg code N, S, O, B, or null");
      }
      monthlyState = monthlyCode === null ? "unknown" : monthlyCode === "N" ? "none" : "within_period";
    } else if (monthlyCode !== undefined) {
      throw new RangeError("daily rows must not set crspMthFacShrFlg");
    }

    const [sharesStatus, adjustedShares, sharesResidual] = quantityResult(
      shares, factor, record.crspFactorStatus,
    );
    const [volumeStatus, adjustedVolume, volumeResidual] = quantityResult(
      volume,
      factor,
      record.crspFactorStatus,
      monthlyState,
    );
    return {
      packageSecurityKey: securityKey,
      date: recordDate,
      frequency: record.frequency,
      crspFactorField: expectedField,
      crspCumulativeShareFactor: factor,
      crspFactorStatus: record.crspFactorStatus,
      crspMthFacShrFlg: record.frequency === "monthly" ? monthlyCode : null,
      crspMonthlyShareFactorState: monthlyState ?? null,
      crspSharesStatus: sharesStatus,
      crspAdjustedSharesOutstandingThousands: adjustedShares,
      crspSharesRoundTripResidual: sharesResidual,
      crspVolumeStatus: volumeStatus,
      crspAdjustedVolumeShares: adjustedVolume,
      crspVolumeRoundTripResidual: volumeResidual,
    };
  });

  if (data.extractCoverage === "full_history" && (
    previousDate !== basisDate || lastFactorStatus !== "observed" || lastFactor !== 1
  )) {
    throw new RangeError("full_history must end on the CRSP basis date with observed factor 1");
  }

  return {
    schemaVersion: SCHEMA_VERSION,
    sourceRelease: data.sourceRelease.trim(),
    packageSecurityKey: expectedSecurityKey,
    crspBasisDate: basisDate,
    extractCoverage: data.extractCoverage,
    roundingDigits: ROUNDING_DIGITS,
    rows,
  };
}
Full-height labplaygroundOpen full screen