The governed definitions this build depends on. Read them first if a term is unfamiliar.
- PrimaryAdjusted PriceAdjusted price is a transformed price expressed on a declared comparison basis after applying specified corporate-action or other adjustment factors.
- ImportantCorporate ActionA corporate action is an issuer or security event that changes rights, quantities, cash flows, identity, or trading terms.
- ImportantCumulative ReturnCumulative return is the total compounded change across a sequence of periodic returns.
- ImportantPrice RelativePrice relative is the ratio of an ending comparable price to its starting comparable price.
- ImportantPrice ReturnPrice return measures the change in an asset's price while excluding cash distributions such as dividends.
- ImportantReturn BaseReturn base is the declared starting value, unit, and economic basis against which a return is measured.
- ImportantRevision LineageAn ordered chain connecting each correction to the exact earlier record it supersedes.
- ImportantSimple ReturnSimple return is the proportional change from a starting value to an ending value over one period.
- ImportantSplit FactorA split factor is the declared conversion ratio between new and old per-share units for a stock split.
- ImportantStock SplitA stock split increases the number of shares per holder while proportionally changing the per-share basis without intentionally creating value.
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.
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 be the signed archived CRSP prc at date . Let be the supplied CRSP cumfacpr from date to selected CRSP base date . The signed CRSP adjusted price is:
At the CRSP base date:
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:
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:
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.
| Date | CRSP prc | CRSP cumfacpr | CRSP calculation | Signed CRSP result | CRSP magnitude |
|---|---|---|---|---|---|
| 2024-06-06 | 120.00 | 0.5 | 60.00 | 60.00 | |
| 2024-06-07 | -123.00 | 0.5 | -61.50 | 61.50 | |
| 2024-06-10 | 60.00 | 1.0 | 60.00 | 60.00 |
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 state | Portable input | CRSP output | Why |
|---|---|---|---|
| observed legacy missing price | CRSP prc = 0, positive CRSP cumfacpr | missing | archived zero is a sentinel, not a zero-dollar trade |
| unknown-exchange gap | known or null CRSP prc, null CRSP cumfacpr | missing factor gap | the raw price may survive, but this package does not carry an adjustment across the gap |
| post-delisting unpriced | null CRSP prc, null CRSP cumfacpr | missing delisting state | a 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 audit flow
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
| Asset | Article placement | Video scene | Source | Status |
|---|---|---|---|---|
| CRSP convention overview | Opening | 1–2 | visuals/static/article-hero.svg | Ready |
| Synthetic CRSP formula | Worked example | 4–5 | visuals/static/worked-example.svg | Ready |
| CRSP missing-state guard | Failure states | 7 | visuals/static/failure-guard.svg | Ready |
| Guided CRSP basis lab | Exploration | 3–7 | visuals/animated/playground.html | Ready |
Full source roles and limitations are recorded in ../REFERENCES.md.
Rendered from the canonical Mermaid sources linked by this article.
Archived CRSPAccess cumulative-price calculation flow
This flow separates CRSP coverage evidence from a calculable CRSP row.
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.
Takeaway: a CRSP adjusted price is a versioned derived field, not free-standing corporate-action truth.
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.
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/mcumfacprfield purpose; CRSP adjustment base date;A(t) = P(t) * C(t);C(base) = 1.0; negativeprcas a bid/ask-average marker; zeroprcwhen 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 factor1.0; factor-type choices; thegapflgchoice 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.
Full dependency-light reference implementations in both supported languages.
/** 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,
};
}
The embedded lab now expands to its full document height, keeping the article as the only scroll surface.