D02-F01-A04 / Complete engineering topic

CRSP Cumulative Price Adjustment: Respect the Vendor Basis, Sign, and Gaps

A production-minded guide to CRSP Cumulative Price Adjustment: Respect the Vendor Basis, Sign, and Gaps.

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

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

A cumulative factor is not just a number beside a price. It names a provider convention, a direction, a target basis, a data version, and a rule for unavailable history. Drop any one of those, and an adjusted chart can look convincing while being impossible to reproduce.

This tutorial implements one precise rule: the archived CRSPAccess ts_print CRSP cumulative price factor. It does not generalize that field into universal corporate-action truth, and it does not claim current CRSP CIZ 2.0 compatibility.

A CRSP raw row, base date, and missing-state guard remain visible in one audit trail

Start with the version boundary

The official archived CRSP Stock and Index Data Description Guide names cumfacpr for daily CRSPAccess data and mcumfacpr for monthly data. It describes a cumulative factor from a base date, used to compare prices across distributions. The same guide defines a selected CRSP adjustment base date and sets its cumulative factor to 1.0.

CRSP's product notice says CRSPAccess and Legacy/Flat File 1.0 production ended with the December 2024 data released in January 2025, after migration to CIZ 2.0. Therefore, this article always says archived CRSPAccess. It does not invent a mapping from legacy cumfacpr to current CIZ fields. (CRSP archived data guide, CRSP CIZ migration notice)

The CRSP direction is multiplication

Let PCRSP(t)P_{\mathrm{CRSP}}(t) be the signed archived CRSP prc at date tt. Let CCRSP(t;b)C_{\mathrm{CRSP}}(t;b) be the supplied CRSP cumfacpr from date tt to selected CRSP base date bb. The signed CRSP adjusted price is:

ACRSP(t;b)=PCRSP(t)CCRSP(t;b)A_{\mathrm{CRSP}}(t;b) = P_{\mathrm{CRSP}}(t)\,C_{\mathrm{CRSP}}(t;b)

At the CRSP base date:

CCRSP(b;b)=1C_{\mathrm{CRSP}}(b;b)=1

Those CRSP names matter. Inverting the factor because another vendor uses the reciprocal would be a different convention.

For a nonmissing, unrounded CRSP row, the reverse check is:

PCRSP(t)=ACRSP(t;b)CCRSP(t;b)P_{\mathrm{CRSP}}(t) = \frac{A_{\mathrm{CRSP}}(t;b)}{C_{\mathrm{CRSP}}(t;b)}

The implementation computes first and then rounds half away from zero to the declared precision. Once the CRSP adjusted result is rounded, reverse division is only a precision check. It is not a reason to discard the raw CRSP prc or CRSP cumfacpr.

The sign is data, not economics

In archived CRSPAccess, a negative CRSP prc marks a bid/ask average used when a closing trade price is unavailable. It is not a negative economic stock price. The package preserves the signed CRSP result and publishes a separate CRSP magnitude:

MCRSP(t;b)=ACRSP(t;b)M_{\mathrm{CRSP}}(t;b) = \left|A_{\mathrm{CRSP}}(t;b)\right|

Applying abs before recording the CRSP price kind destroys provenance. A research filter may later choose trade-only rows, but that decision should use the retained CRSP source state.

A synthetic CRSPAccess-compatible calculation

The example is deliberately synthetic. The CRSP base date is 2024-06-10, and CRSP cumfacpr equals 1.0 there. A fabricated split-like CRSP event has EXDT = 2024-06-10 and FACPR = 1.0.

DateCRSP prcCRSP cumfacprCRSP calculationSigned CRSP resultCRSP magnitude
2024-06-06120.000.5120.00×0.5120.00\times0.560.0060.00
2024-06-07-123.000.5123.00×0.5-123.00\times0.5-61.5061.50
2024-06-1060.001.060.00×1.060.00\times1.060.0060.00

The synthetic CRSP base-date calculation preserves the quote sign

The archived CRSP guide says a simple two-for-one split has distribution CRSP FACPR = 1.0, while the factor used inside the period return is CRSP FACPR + 1 = 2.0. That statement does not make every CRSP cumulative factor 2.0: CRSP cumfacpr is tied to its selected base date and direction. The code consumes the supplied cumulative field; it never rebuilds it from the event ratio.

Missing CRSP rows must stay missing

There are three different absent-result states in the reference contract:

CRSP statePortable inputCRSP outputWhy
observed legacy missing priceCRSP prc = 0, positive CRSP cumfacprmissingarchived zero is a sentinel, not a zero-dollar trade
unknown-exchange gapknown or null CRSP prc, null CRSP cumfacprmissing factor gapthe raw price may survive, but this package does not carry an adjustment across the gap
post-delisting unpricednull CRSP prc, null CRSP cumfacprmissing delisting statea price factor does not invent terminal proceeds

The official archived CRSP guide says adjusted values are missing when a trading gap with possibly unknown split events lies between an observation and its base date. The archived CRSP Programmer's Guide also exposes a choice to carry or stop; this package pins that choice to stop. (CRSP Programmer's Guide)

The CRSP guard separates a price sentinel, factor gap, and unpriced delisting

The audit flow

Rendering system map…

Before calculating, the implementation requires the exact CRSP convention label, source version, CRSP base date, stop-at-gap policy, ordered dates, and an anchor factor of 1.0. The Python and TypeScript implementations consume one shared fixture and return the same CRSP-named diagnostics.

Explore the CRSP base date and failure states

Use the guided CRSP basis lab. The canonical scenario contains 36 synthetic rows. Step across the CRSP base date, inspect the negative quote marker, and compare missing-price, factor-gap, and post-delisting states. Switch to the factor-gap and delisting scenarios to see why a plausible carried value is rejected. Back, Step, Play, Pause, Reset, and speed all use the same deterministic transition function.

Why there is no named historical issuer

A real example needs more than an issuer's public split announcement. To support a statement such as “CRSP adjusted this issuer from X to Y,” the evidence must include licensed, redistributable CRSP prc and cumfacpr rows, the CRSP base date, the exact product and data cut, the CRSP gap setting, and the matching CRSP event fields. Those field-level observations were not available for public redistribution in this review.

Another provider's price history or split ratio cannot validate a CRSP-specific field as though the providers were interchangeable. The historical example is therefore deferred, and every example here is labeled synthetic. CRSP's public policy page also makes clear that database use and dissemination are governed by subscriber terms. (CRSP Policies & Statements)

What to test in production

  • reject a CRSP base date without factor 1.0;
  • reject duplicate or reversed CRSP dates;
  • reject a positive value labeled as a CRSP bid/ask average;
  • reject a nonzero value labeled as the archived CRSP missing sentinel;
  • keep CRSP cumulative-factor gaps and post-delisting rows missing;
  • preserve raw CRSP fields, product version, data cut, base date, and extraction time;
  • compare rounded CRSP results within the declared precision, not as exact binary decimals;
  • run a separate, documented CIZ 2.0 mapping review before using current files.

Passing these tests proves that code follows this declared archived CRSPAccess contract. It does not prove that a licensed CRSP data cut is complete, that a corporate action was economically reconstructed, or that an adjusted series is suitable for trading.

Summary

The archived CRSPAccess cumulative price adjustment multiplies signed CRSP prc by supplied CRSP cumfacpr on a declared CRSP base-date basis. Preserve the quote sign, keep zero and gaps missing, stop across unknown event coverage, retain the source version, and never substitute another provider's convention.

Continue with CRSP Cumulative Share/Volume Adjustment, where the CRSP direction and units differ and must again remain vendor-named.

Asset map

AssetArticle placementVideo sceneSourceStatus
CRSP convention overviewOpening1–2visuals/static/article-hero.svgReady
Synthetic CRSP formulaWorked example4–5visuals/static/worked-example.svgReady
CRSP missing-state guardFailure states7visuals/static/failure-guard.svgReady
Guided CRSP basis labExploration3–7visuals/animated/playground.htmlReady

Full source roles and limitations are recorded in ../REFERENCES.md.

Archived CRSPAccess cumulative-price calculation flow

This flow separates CRSP coverage evidence from a calculable CRSP row.

Rendering system map…

Takeaway: the package multiplies only an observed archived CRSPAccess price by a supplied CRSP cumulative factor on a declared base-date basis.

CRSP evidence and result lifecycle

This state model prevents a derived CRSP result from losing its product, base-date, and licensing boundary.

Rendering system map…

Takeaway: a CRSP adjusted price is a versioned derived field, not free-standing corporate-action truth.

References4 primary sources and evidence notes

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

All sources were checked on 2026-07-22. This package uses public documentation to explain an archived CRSPAccess convention, but it does not redistribute licensed CRSP observations.

R1 — CRSP Stock and Index Data Description Guide, CRSPAccess

  • Organization: Center for Research in Security Prices (CRSP)
  • Source type: Official archived product data dictionary and calculation guide
  • Version/date: Official PDF states Document last modified February 12, 2026; the fields and interfaces it describes remain the archived CRSPAccess product convention
  • URL: https://www.crsp.org/crsp_pdf/crsp-us-stock-indexes-databases-data-descriptions-guide-crspaccess/
  • Jurisdiction: CRSP US Stock Database
  • Supports: CRSP cumfacpr/mcumfacpr field purpose; CRSP adjustment base date; A(t) = P(t) * C(t); C(base) = 1.0; negative prc as a bid/ask-average marker; zero prc when neither a price nor bid/ask average is available; ex-distribution dates; FACPR; missing adjusted values across gaps when possible split events are unknown; delisting caveats.
  • Limitations: The PDF revision is current as of this review, but the documented CRSPAccess product convention is legacy and not a statement of current CRSP CIZ 2.0 schema or behavior. Formula typography in the PDF is difficult to extract, so the package cites the surrounding definitions and reproduces only the documented relationship.

R2 — CRSP Programmer's Guide

  • Organization: Center for Research in Security Prices (CRSP)
  • Source type: Official archived technical documentation
  • Publication or effective date: Updated July 17, 2023
  • URL: https://www.crsp.org/wp-content/uploads/2023/09/programmers-guide.pdf
  • Jurisdiction: CRSP US Stock and US Index Databases
  • Supports: crsp_adj_load; a base anchor date guaranteed to have factor 1.0; factor-type choices; the gapflg choice to carry or stop across an unknown-exchange gap; multiply direction for prices and divide direction for shares/values in the archived array utility.
  • Limitations: The guide documents legacy CRSPAccess programming interfaces. This package implements only one transparent ts_print-style price transformation and does not wrap the CRSP API.

R3 — Important Notice: CRSP US Stock & Indexes Databases Flat File Format 2.0 (CIZ)

  • Organization: Center for Research in Security Prices (CRSP)
  • Source type: Official product migration notice
  • Publication date: May 15, 2025
  • URL: https://www.crsp.org/important-notice-crsp-us-stock-amp-indexes-databases-flat-file-format-2-0-ciz/
  • Jurisdiction: CRSP US Stock and Indexes databases
  • Supports: CIZ 2.0 was introduced in 2022; the December 2024 data released in January 2025 was the last Legacy FIZ/SIZ release; the same release was the last for CRSPAccess.
  • Limitations: The notice establishes the version boundary; it does not map every legacy field to CIZ 2.0.

R4 — CRSP Policies & Statements

  • Organization: Center for Research in Security Prices (CRSP)
  • Source type: Official terms, citation, and licensing statement
  • URL: https://www.crsp.org/policies-statements/
  • Jurisdiction: CRSP products and documentation
  • Supports: CRSP data and product-guide use is governed by subscriber agreements; dissemination of research based on CRSP data requires attribution; non-subscribers must obtain permission for use of CRSP data or proprietary information.
  • Limitations: License terms depend on the subscriber agreement. This package avoids redistributing CRSP observations and uses synthetic values.

Historical-example evidence decision

No named-issuer historical example is published. The public official guides establish the CRSP field semantics, but this review did not obtain a CRSP-licensed, redistributable row set containing the exact prc, cumfacpr, base date, product/data-cut identity, gap settings, and matching event record. A split ratio or price history from another provider would not validate a CRSP-specific cumulative factor. Until those CRSP fields can be cited and redistributed under the applicable license, the worked case remains explicitly synthetic.

crsp_cumulative_price_adjustment.ts
/** Archived CRSPAccess cumulative-price adjustment teaching reference. */

export const CRSP_CONVENTION = "CRSPAccess-ts_print-cumfacpr-archived" as const;
export const CRSP_GAP_POLICY = "stop-at-unknown-exchange" as const;

type CoverageStatus = "observed" | "unknown_exchange_gap" | "post_delisting_unpriced";
type CrspPriceKind = "trade" | "bid_ask_average" | "missing";

export interface CrspFactorEvent {
  crspExdt: string;
  crspFacpr: number;
  syntheticLabel: string;
}

export interface CrspPriceRecord {
  date: string;
  crspPrc: number | null;
  crspCumfacpr: number | null;
  crspPriceKind: CrspPriceKind;
  coverageStatus: CoverageStatus;
}

export interface CrspAdjustmentInput {
  crspConvention: typeof CRSP_CONVENTION;
  crspSourceVersion: string;
  packageSecurityKey: `SYNTHETIC-${string}`;
  crspBaseDate: string;
  crspGapPolicy: typeof CRSP_GAP_POLICY;
  roundingDecimals?: number;
  crspFactorEvents: CrspFactorEvent[];
  records: CrspPriceRecord[];
}

type JsonObject = Record<string, unknown>;
const COVERAGE = new Set<CoverageStatus>(["observed", "unknown_exchange_gap", "post_delisting_unpriced"]);
const PRICE_KINDS = new Set<CrspPriceKind>(["trade", "bid_ask_average", "missing"]);

function objectValue(value: unknown, name: string): JsonObject {
  if (!value || typeof value !== "object" || Array.isArray(value)) throw new TypeError(`${name} must be an object`);
  return value as JsonObject;
}

function numberValue(value: unknown, name: string): number {
  if (typeof value !== "number" || !Number.isFinite(value)) throw new TypeError(`${name} must be a finite number`);
  return value;
}

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 a real ISO date`);
  return value;
}

function roundHalfAway(value: number, decimals: number): number {
  const scale = 10 ** decimals;
  const scaled = value * scale;
  const rounded = scaled >= 0 ? Math.floor(scaled + 0.5) : Math.ceil(scaled - 0.5);
  return rounded / scale;
}

function validateEvent(value: unknown, index: number): CrspFactorEvent {
  const item = objectValue(value, `crspFactorEvents[${index}]`);
  const crspExdt = isoDate(item.crspExdt, `crspFactorEvents[${index}].crspExdt`);
  const crspFacpr = numberValue(item.crspFacpr, `crspFactorEvents[${index}].crspFacpr`);
  if (typeof item.syntheticLabel !== "string" || !item.syntheticLabel.trim()) throw new TypeError(`crspFactorEvents[${index}].syntheticLabel must be non-empty`);
  return { crspExdt, crspFacpr, syntheticLabel: item.syntheticLabel };
}

export function calculate(input: CrspAdjustmentInput | unknown) {
  const source = objectValue(input, "data");
  if (source.crspConvention !== CRSP_CONVENTION) throw new RangeError(`crspConvention must equal ${CRSP_CONVENTION}`);
  if (source.crspGapPolicy !== CRSP_GAP_POLICY) throw new RangeError(`crspGapPolicy must equal ${CRSP_GAP_POLICY}`);
  if (typeof source.crspSourceVersion !== "string" || !source.crspSourceVersion.trim()) throw new TypeError("crspSourceVersion must be non-empty");
  if (typeof source.packageSecurityKey !== "string" || !source.packageSecurityKey.startsWith("SYNTHETIC-")) throw new RangeError("packageSecurityKey must be an unmistakably synthetic identifier");
  const crspBaseDate = isoDate(source.crspBaseDate, "crspBaseDate");
  const decimals = source.roundingDecimals === undefined ? 6 : source.roundingDecimals;
  if (!Number.isInteger(decimals) || (decimals as number) < 0 || (decimals as number) > 12) throw new RangeError("roundingDecimals must be an integer from 0 through 12");
  if (!Array.isArray(source.crspFactorEvents)) throw new TypeError("crspFactorEvents must be a list");
  const events = source.crspFactorEvents.map(validateEvent);
  const eventDates = events.map((event) => event.crspExdt);
  if (eventDates.some((value, index) => index > 0 && value < eventDates[index - 1])) throw new RangeError("crspFactorEvents must be ordered by crspExdt");
  if (!Array.isArray(source.records) || source.records.length === 0) throw new TypeError("records must be a non-empty list");

  const outputRecords: JsonObject[] = [];
  const dates: string[] = [];
  const factorChangeDates: string[] = [];
  let baseFactorSeen = false;
  let priorFactor: number | null = null;

  source.records.forEach((rawRecord, index) => {
    const record = objectValue(rawRecord, `records[${index}]`);
    const recordDate = isoDate(record.date, `records[${index}].date`);
    if (dates.length && recordDate <= dates[dates.length - 1]) throw new RangeError("records must have unique, strictly increasing dates");
    dates.push(recordDate);
    if (!COVERAGE.has(record.coverageStatus as CoverageStatus)) throw new RangeError(`records[${index}].coverageStatus is unsupported`);
    if (!PRICE_KINDS.has(record.crspPriceKind as CrspPriceKind)) throw new RangeError(`records[${index}].crspPriceKind is unsupported`);
    const coverage = record.coverageStatus as CoverageStatus;
    const priceKind = record.crspPriceKind as CrspPriceKind;
    let adjusted: number | null;
    let magnitude: number | null;
    let recovered: number | null;
    let status: string;

    if (coverage !== "observed") {
      if (record.crspCumfacpr !== null) throw new RangeError("coverage gaps require null crspCumfacpr");
      if (coverage === "post_delisting_unpriced" && (record.crspPrc !== null || priceKind !== "missing")) throw new RangeError("an unpriced CRSP delisting requires null crspPrc and missing crspPriceKind");
      if (coverage === "unknown_exchange_gap" && record.crspPrc === null && priceKind !== "missing") throw new RangeError("a null CRSP gap price requires missing crspPriceKind");
      if (coverage === "unknown_exchange_gap" && record.crspPrc !== null) {
        const gapPrice = numberValue(record.crspPrc, `records[${index}].crspPrc`);
        if (priceKind === "trade" && gapPrice <= 0) throw new RangeError("CRSP trade prc must be positive");
        if (priceKind === "bid_ask_average" && gapPrice >= 0) throw new RangeError("CRSP bid/ask-average prc must retain its negative sign marker");
        if (priceKind === "missing" && gapPrice !== 0) throw new RangeError("archived CRSPAccess missing prc must be zero");
      }
      status = coverage === "unknown_exchange_gap" ? "CRSP_FACTOR_GAP" : "CRSP_POST_DELISTING_UNPRICED";
      adjusted = magnitude = recovered = null;
    } else {
      const price = numberValue(record.crspPrc, `records[${index}].crspPrc`);
      const factor = numberValue(record.crspCumfacpr, `records[${index}].crspCumfacpr`);
      if (factor <= 0) throw new RangeError("observed CRSP cumfacpr must be positive");
      if (priceKind === "trade" && price <= 0) throw new RangeError("CRSP trade prc must be positive");
      if (priceKind === "bid_ask_average" && price >= 0) throw new RangeError("CRSP bid/ask-average prc must retain its negative sign marker");
      if (priceKind === "missing" && price !== 0) throw new RangeError("archived CRSPAccess missing prc must be zero");
      if (priceKind !== "missing" && price === 0) throw new RangeError("zero CRSP prc must be labeled missing");
      if (recordDate === crspBaseDate) {
        if (factor !== 1) throw new RangeError("CRSP base-date cumfacpr must equal 1.0");
        baseFactorSeen = true;
      }
      if (priorFactor !== null && factor !== priorFactor) factorChangeDates.push(recordDate);
      priorFactor = factor;
      if (priceKind === "missing") {
        status = "CRSP_PRICE_MISSING";
        adjusted = magnitude = recovered = null;
      } else {
        const unrounded = price * factor;
        adjusted = roundHalfAway(unrounded, decimals as number);
        magnitude = roundHalfAway(Math.abs(unrounded), decimals as number);
        recovered = roundHalfAway(adjusted / factor, decimals as number);
        status = priceKind === "trade" ? "CRSP_ADJUSTED_TRADE" : "CRSP_ADJUSTED_BID_ASK_AVERAGE";
      }
    }
    outputRecords.push({
      date: recordDate,
      crspPrc: record.crspPrc,
      crspCumfacpr: record.crspCumfacpr,
      crspAdjustedPrice: adjusted,
      crspAdjustedMagnitude: magnitude,
      crspRecoveredPrc: recovered,
      crspPriceKind: priceKind,
      coverageStatus: coverage,
      status,
    });
  });

  if (!baseFactorSeen) throw new RangeError("records must contain an observed CRSP base-date row with cumfacpr 1.0");
  if (events.some((event) => event.crspExdt < dates[0] || event.crspExdt > dates[dates.length - 1])) throw new RangeError("CRSP factor event dates must fall inside the record range");
  return {
    crspConvention: CRSP_CONVENTION,
    crspSourceVersion: source.crspSourceVersion,
    packageSecurityKey: source.packageSecurityKey,
    crspBaseDate,
    crspGapPolicy: CRSP_GAP_POLICY,
    crspPriceFormula: "CRSP adjusted price = CRSP PRC * CRSP CUMFACPR",
    crspFactorEvents: events,
    crspFactorChangeDates: factorChangeDates,
    roundingDecimals: decimals,
    records: outputRecords,
  };
}
Full-height labplaygroundOpen full screen