The governed definitions this build depends on. Read them first if a term is unfamiliar.
- PrimaryDelistingDelisting is the removal of a security's listing from a trading venue.
- PrimaryHolding Period ReturnHolding period return measures the economic gain or loss relative to starting value over a specified ownership interval.
- PrerequisiteCorporate ActionA corporate action is an issuer or security event that changes rights, quantities, cash flows, identity, or trading terms.
- ImportantData LineageData lineage records where data came from, how it was transformed, and which entities, activities, and agents affected it.
- ImportantData ReconciliationData reconciliation is the controlled comparison of records or aggregates across sources or processing stages to explain differences.
- ImportantListingA listing is the effective-dated relationship that makes a security available under specified terms on a trading venue.
- ImportantMissing ValueA missing value indicates that the expected information is unavailable, unobserved, inapplicable, or withheld under a stated data contract.
- ImportantPoint-in-Time DatasetA point-in-time dataset can reconstruct what information was valid and available to a specified user or system at a historical decision time.
- ImportantSecurity MasterA security master is a governed reference-data system that organizes instruments, issuers, identifiers, listings, currencies, statuses, and effective dates.
- ImportantStable Security IdentifierA stable security identifier is a governed key intended to preserve instrument identity across changes in mutable labels such as ticker or name.
- ImportantTotal ReturnTotal return measures price change together with eligible distributions over a declared holding period.
- ImportantUniverse MembershipUniverse membership is an effective-dated assertion that a security belongs to a defined eligible set for a specified interval.
- ImportantValid ZeroA valid zero is an observed or derived numeric value of exactly zero that is meaningful under the field's contract.
- MentionedAcquisitionAn acquisition is a transaction in which one entity obtains control of another business or its assets under stated terms.
- MentionedMergerA merger is a corporate transaction combining entities or securities under legally specified consideration and continuity terms.
A security can stop trading before its economic story is finished. Cash may arrive later. Successor shares may replace the old security. A liquidation may pay several distributions. Or the final value may still be unknown.
The practical goal is not to fill every blank. It is to build a return history that tells the truth about what was observed, what was only estimated, and what was unavailable at the time.
The dangerous shortcut
Suppose a security's last regular close is USD 50. Three shortcuts all look convenient:
- end the series at USD 50 and ignore later value;
- assume the delisting means a total loss;
- insert a research haircut and present it as observed return.
Each answer may be wrong for a different reason. The correct system needs a state model before it needs a formula.
The left path in the visual is allowed to create an observed terminal return only when proceeds evidence is complete. The right path can support sensitivity analysis, but it remains labeled as a research estimate forever.
Four proceeds states, not one nullable number
This package represents terminal evidence as one of four states:
| State | What is known | What the algorithm may calculate |
|---|---|---|
complete | Every proceeds leg required by the declared evidence set | Observed terminal and linked returns |
partial | At least one observed cash or stock leg, but completeness is unresolved | Known subtotal only; returns stay null |
unknown | No defensible terminal value | No terminal value or return |
zero_confirmed | A sourced statement establishes no shareholder proceeds | Observed terminal return of -1 |
This distinction is consistent with an important lesson from the CRSPAccess data guide: its own provider-specific process distinguishes usable post-delisting prices or distributions, evidence of worthlessness, and missing return states. We do not copy CRSP's codes or ten-period rules. We preserve the more general discipline that missing evidence is not a numeric return.
Separate the two return intervals
Let be the previous regular close and the final regular close. The last regular return is:
Now value the terminal cash and stock legs in one base currency:
Here, is cash per old share, is successor shares per old share, is the documented successor valuation, and is base-currency units per source-currency unit. Subtract explicit fees and withholding :
Only complete observed evidence permits:
The linked return covers both intervals:
The formula is simple. Establishing that is complete, observed, and eligible at the requested point in time is the real algorithm.
A fully synthetic worked example
Use two regular prices: USD 48 and USD 50. The event later delivers two proceeds legs:
- EUR 10 cash at 1.10 USD per EUR, worth USD 11;
- 0.8 successor share valued at USD 52.50, worth USD 42.
Gross observed value is USD 53. Fees and withholding total USD 0.50, leaving USD 52.50 net.
The results are:
last regular return = 50 / 48 - 1 = 0.041666666667
terminal return = 52.5 / 50 - 1 = 0.050000000000
linked return = 52.5 / 48 - 1 = 0.093750000000
Now change only the evidence state from complete to partial. The known USD 52.50 subtotal remains in the audit output, but both returns become null. The numeric subtotal did not change; confidence in its completeness did.
Time has four meanings
A single date cannot answer every point-in-time question. The implementation preserves:
- the economic clock:
tradingAt,effectiveAt,valuationAt, orrateAt; observedAt: when the underlying value or evidence was observed;sourceAt: the timestamp assigned or published by the named source;availableAt: when the system could actually use the record.
For price, proceeds, successor valuation, and FX records, the ordering is:
economic time <= observedAt <= sourceAt <= availableAt
An event announcement is different: its observation can precede a future effectiveAt. The event may be known but cannot affect the observed result until it is effective.
The SEC's Form 25 timing interpretation is a concrete reminder that filing and effective dates can differ. It does not supply terminal proceeds, which is why an event notice and an economic return must remain separate records.
Event reason is not a valuation rule
The implementation accepts merger, tender offer, bankruptcy, liquidation, exchange transfer, voluntary, regulatory, and other as operational reason labels. They are not a universal exchange taxonomy, and none implies a payout.
A merger can have complete cash-and-stock terms or unresolved contingent value. A liquidation can have partial distributions. A bankruptcy can eventually have recovery or an evidenced zero. The reason helps route research; it never supplies the number.
How the reference code fails safely
The Python and TypeScript versions use the same JSON fixture and exact scaled-integer arithmetic. They reject:
- numbers instead of decimal strings;
- negative money, more than 12 decimal places, or malformed timestamps;
- noncausal price, proceeds, valuation, FX, source, and availability clocks;
- mismatched currencies or an unidentified successor security;
- a complete state with no proceeds leg;
- a partial state with no known leg;
- a zero state without explicit evidence;
- deductions larger than observed proceeds.
The input is never mutated. Event metadata is withheld before the event record's availableAt, preventing a diagnostic field from leaking future knowledge.
Research estimates belong in their own column
Research often needs sensitivity cases. A model might assign an estimated net value of USD 5 per old share while real proceeds are unresolved. The package permits that only with a method ID, caveat, source, timestamps, currency, and FX record.
It returns:
{
"observed": {
"status": "missing_terminal_proceeds",
"terminalReturn": null
},
"researchEstimate": {
"status": "research_estimate",
"terminalReturn": "-0.900000000000"
}
}
If complete observed proceeds later arrive, the estimate does not disappear into them. Both branches remain separate, which lets a researcher measure estimation error without rewriting the historical evidence state.
Explore the state machine
Open the guided playground. Use Step to move from regular prices through event eligibility and proceeds evidence. Compare complete, partial, unknown, explicit-zero, and estimate scenarios. Reset returns to the same useful starting state, and the speed control changes Play timing without changing Step semantics.
When a real historical example is defensible
A named case would make this lesson more concrete, but only if public primary evidence establishes every input used in the arithmetic. At minimum, require:
- predecessor and successor identities;
- event, revision, and effective timestamps;
- last regular price with venue and session;
- every cash, share, fractional, contingent, and later distribution term;
- successor valuation and FX policies plus observations;
- gross-versus-net treatment;
- point-in-time availability for every record.
Nasdaq's Daily List can evidence a Nasdaq delisting notification. The SEC's Exchange Delistings page points researchers to Form 25 records. Neither one, by itself, proves terminal proceeds or a return. This article therefore uses synthetic arithmetic and makes no historical company-return claim.
What to carry into production
Preserve raw records and revisions. Keep gross and net views explicit. Treat vendor conventions as scoped adapters. Never interpret a vendor missing code as a numeric return. Require explicit evidence for zero. Keep estimates labeled after later evidence arrives.
You can now reconstruct the boundary without confusing the last regular return, observed terminal return, and research estimate. Continue with Historical Constituent Reconstruction to apply the same point-in-time discipline to membership histories.
Asset map
| Asset | Teaching role | Article placement | Video scene | Source | Fallback |
|---|---|---|---|---|---|
| V1 | Observed versus estimate paths | Opening | 1-2 | visuals/static/article-hero.svg | Article state table |
| V2 | Exact synthetic arithmetic | Worked example | 5 | visuals/static/worked-example.svg | Printed arithmetic |
| V3 | Missing-is-not-zero guard | Safe failure | 7 | visuals/static/failure-guard.svg | Failure list |
| V4 | Evidence decision flow | Time and states | 3-4 | visuals/mermaid/calculation-flow.md | Numbered algorithm |
| V5 | Point-in-time lifecycle | Time and states | 4 | visuals/mermaid/state-lifecycle.md | Clock explanation |
| V6 | Scenario step-through | Exploration | 4-8 | visuals/animated/playground.html | V1-V3 static SVGs |
Primary source roles and limitations are in REFERENCES.md. This is engineering education, not investment, legal, tax, or accounting advice.
Rendered from the canonical Mermaid sources linked by this article.
Evidence-gated calculation flow
This flow separates evidence eligibility from arithmetic and keeps research imputation outside the observed branch.
Takeaway: an event reason or a numerical subtotal never bypasses the completeness and point-in-time gates.
Point-in-time evidence lifecycle
The state diagram shows why known, effective, partial, complete, and estimated are different states.
Takeaway: later complete evidence may coexist with the earlier research estimate; it does not retroactively turn that estimate into an observed fact.
References5 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 accessed on 2026-07-22. The worked examples and datasets in this package are synthetic. No licensed vendor observations, private API results, or historical security returns are reproduced.
R1 - CRSPAccess US Stock and Index Databases Data Descriptions Guide
- Organization: Center for Research in Security Prices (CRSP), now part of Morningstar Indexes
- Source type: Official vendor data definition and calculation guide
- Publication or effective date: Document last modified February 12, 2026
- Version: CRSPAccess (FIZ)
- URL: https://www.crsp.org/crsp_pdf/crsp-us-stock-indexes-databases-data-descriptions-guide-crspaccess/
- Jurisdiction and scope: CRSP-followed US exchanges and CRSP's own research database conventions
- Supports: The distinction between the last exchange price, amount after delisting, payment date, off-exchange price or quote, distributions, evidence-backed zero, and missing delisting-return states. The guide defines CRSP's delisting return as amount after delisting compared with the last trading price.
- Limitations: CRSP conventions, missing-value codes, ten-period rules, and research closure rules are provider-specific. They are evidence that missingness and provenance matter, not universal rules for every market or vendor. Access to underlying CRSP observations may require a license.
R2 - CRSP US Stock and Indexes Databases Data Descriptions Guide
- Organization: Center for Research in Security Prices (CRSP)
- Source type: Official current-format data definition and calculation guide
- Publication or effective date: Current guide accessed July 22, 2026
- URL: https://www.crsp.org/wp-content/uploads/guides/CRSP_US_Stock_%26_Indexes_Database_Data_Descriptions_Guide.pdf
- Jurisdiction and scope: CRSP US stock database
- Supports: A valid post-delisting value may come from an eligible price or a complete set of payments; insufficient information remains missing; researchers can choose an alternate estimate, but that choice is not the same as observed provider evidence.
- Limitations: The guide documents CRSP products. This package does not implement or claim parity with CRSP.
R3 - Nasdaq Daily List Product Description
- Organization: Nasdaq
- Source type: Official exchange data-product description
- Publication or effective date: Current page accessed July 22, 2026
- URL: https://classic.nasdaqtrader.com/Trader.aspx?id=DailyListPD
- Jurisdiction and scope: Nasdaq-listed securities
- Supports: Nasdaq's Daily List carries delisting notifications and other listing and corporate-action records; the page says historical Daily List coverage dates to 1999.
- Limitations: A delisting notice establishes an event record, not the shareholder's terminal proceeds, last regular price, FX conversion, or a reconstructed return. Product access may require a subscription.
R4 - SEC Exchange Delistings
- Organization: U.S. Securities and Exchange Commission
- Source type: Official regulator search guidance
- Publication or effective date: Current page accessed July 22, 2026
- URL: https://www.sec.gov/rules-regulations/exchange-delistings
- Jurisdiction and scope: US Exchange Act delisting notices
- Supports: Form 25 and Form 25-NSE filings are public evidence for exchange delisting events.
- Limitations: A Form 25 is not evidence of terminal cash, successor-share value, a final distribution, or zero recovery.
R5 - SEC Exchange Act Forms, Section 111.01
- Organization: U.S. Securities and Exchange Commission, Division of Corporation Finance
- Source type: Official staff compliance interpretation
- Publication or effective date: Interpretation dated September 30, 2008; page current when accessed
- URL: https://www.sec.gov/rules-regulations/staff-guidance/corporation-finance-interpretations/exchange-act-forms
- Jurisdiction and scope: US Exchange Act Form 25 timing
- Supports: The cited interpretation states that a Form 25 delisting effective date cannot be earlier than ten days after filing. This is one example of why source, effective, and availability clocks must not be collapsed.
- Limitations: This timing statement does not determine an economic terminal value or return and is not generalized outside its stated regulatory scope.
Evidence boundary for historical examples
A named historical case is publication-ready only when public, reproducible primary evidence establishes all inputs used in its arithmetic: security identity, last regular price and timestamp, event and effective time, every cash or stock proceeds leg, successor valuation policy and observation, FX, deductions, and the time each item became usable. No case meeting that full evidence bar is asserted here. The synthetic case demonstrates the algorithm without turning incomplete historical clues into factual market output.
Full dependency-light reference implementations in both supported languages.
/** Evidence-gated delisting return reconstruction with exact scaled integers. */
const SCALE = 1_000_000_000_000n;
const DECIMAL_RE = /^(0|[1-9]\d{0,14})(?:\.(\d{1,12}))?$/;
const UTC_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/;
const CURRENCY_RE = /^[A-Z]{3}$/;
const ID_RE = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
const REASONS = new Set(["merger", "tender_offer", "bankruptcy", "liquidation", "exchange_transfer", "voluntary", "regulatory", "other"]);
const PROCEEDS_STATUSES = new Set(["complete", "partial", "unknown", "zero_confirmed"]);
const mapping = (value: any, name: string): Record<string, any> => {
if (!value || typeof value !== "object" || Array.isArray(value)) throw new TypeError(`${name} must be an object`);
return value;
};
const text = (value: any, name: string, minimum = 1, maximum = 500): string => {
if (typeof value !== "string" || value.trim().length < minimum || value.trim().length > maximum) throw new TypeError(`${name} must be a non-empty string`);
return value;
};
const identifier = (value: any, name: string): string => {
const result = text(value, name, 1, 128);
if (!ID_RE.test(result)) throw new RangeError(`${name} has invalid syntax`);
return result;
};
const currency = (value: any, name: string): string => {
if (typeof value !== "string" || !CURRENCY_RE.test(value)) throw new RangeError(`${name} must be a three-letter uppercase code`);
return value;
};
const timestamp = (value: any, name: string): number => {
if (typeof value !== "string" || !UTC_RE.test(value)) throw new RangeError(`${name} must use YYYY-MM-DDTHH:MM:SSZ`);
const result = Date.parse(value);
if (!Number.isFinite(result) || new Date(result).toISOString().replace(".000Z", "Z") !== value) throw new RangeError(`${name} is not a valid UTC timestamp`);
return result;
};
const decimal = (value: any, name: string, positive = false): bigint => {
if (typeof value !== "string") throw new TypeError(`${name} must be a decimal string`);
const match = DECIMAL_RE.exec(value);
if (!match) throw new RangeError(`${name} must be a non-negative decimal with at most 12 places`);
const [whole, fraction = ""] = value.split(".");
const result = BigInt(whole) * SCALE + BigInt(fraction.padEnd(12, "0") || "0");
if (positive && result === 0n) throw new RangeError(`${name} must be positive`);
return result;
};
const format = (input: bigint): string => {
const sign = input < 0n ? "-" : "";
const value = input < 0n ? -input : input;
return `${sign}${value / SCALE}.${(value % SCALE).toString().padStart(12, "0")}`;
};
const roundDiv = (numerator: bigint, denominator: bigint): bigint => {
if (denominator <= 0n) throw new RangeError("denominator must be positive");
const sign = numerator < 0n ? -1n : 1n;
const absolute = numerator < 0n ? -numerator : numerator;
return sign * ((absolute + denominator / 2n) / denominator);
};
const mul = (left: bigint, right: bigint): bigint => roundDiv(left * right, SCALE);
const ratio = (left: bigint, right: bigint): bigint => roundDiv(left * SCALE, right);
const sourceTimes = (record: Record<string, any>, path: string): [number, number] => {
identifier(record.sourceId, `${path}.sourceId`);
const observed = timestamp(record.observedAt, `${path}.observedAt`);
const source = timestamp(record.sourceAt, `${path}.sourceAt`);
const available = timestamp(record.availableAt, `${path}.availableAt`);
if (observed > source || source > available) throw new RangeError(`${path} times must satisfy observedAt <= sourceAt <= availableAt`);
return [observed, available];
};
const priceObservation = (value: any, path: string, baseCurrency: string) => {
const record = mapping(value, path);
identifier(record.securityId, `${path}.securityId`);
const price = decimal(record.price, `${path}.price`, true);
if (currency(record.currency, `${path}.currency`) !== baseCurrency) throw new RangeError(`${path}.currency must equal baseCurrency`);
const trading = timestamp(record.tradingAt, `${path}.tradingAt`);
const [observed, available] = sourceTimes(record, path);
if (trading > observed) throw new RangeError(`${path}.tradingAt must not follow observedAt`);
return { price, trading, available };
};
const fx = (value: any, path: string, fromCurrency: string, baseCurrency: string): [bigint, number] => {
const record = mapping(value, path);
if (currency(record.fromCurrency, `${path}.fromCurrency`) !== fromCurrency) throw new RangeError(`${path}.fromCurrency does not match its amount currency`);
if (currency(record.toCurrency, `${path}.toCurrency`) !== baseCurrency) throw new RangeError(`${path}.toCurrency must equal baseCurrency`);
const rate = decimal(record.rate, `${path}.rate`, true);
const rateAt = timestamp(record.rateAt, `${path}.rateAt`);
const [observed, available] = sourceTimes(record, path);
if (rateAt > observed) throw new RangeError(`${path}.rateAt must not follow observedAt`);
return [rate, available];
};
const moneyValue = (value: any, path: string, baseCurrency: string): [bigint, Array<[string, number]>] => {
const record = mapping(value, path);
const amount = decimal(record.amountPerOldShare, `${path}.amountPerOldShare`);
const unit = currency(record.currency, `${path}.currency`);
const effective = timestamp(record.effectiveAt, `${path}.effectiveAt`);
const [observed, available] = sourceTimes(record, path);
if (effective > observed) throw new RangeError(`${path}.effectiveAt must not follow observedAt`);
const [rate, fxAvailable] = fx(record.fxToBase, `${path}.fxToBase`, unit, baseCurrency);
return [mul(amount, rate), [[path, available], [`${path}.fxToBase`, fxAvailable]]];
};
const observedResult = (status: string, lastReturn: bigint | null, values: any = {}) => ({
status,
lastRegularReturn: lastReturn === null ? null : format(lastReturn),
terminalGrossValuePerOldShare: values.gross === undefined ? null : format(values.gross),
deductionsPerOldShare: values.deductions === undefined ? null : format(values.deductions),
terminalNetValuePerOldShare: values.net === undefined ? null : format(values.net),
terminalReturn: values.terminalReturn === undefined ? null : format(values.terminalReturn),
linkedReturn: values.linkedReturn === undefined ? null : format(values.linkedReturn),
proceedsBasis: values.basis ?? null,
knownObservedGrossValuePerOldShare: values.knownGross === undefined ? null : format(values.knownGross),
knownObservedDeductionsPerOldShare: values.knownDeductions === undefined ? null : format(values.knownDeductions),
knownObservedNetValuePerOldShare: values.knownNet === undefined ? null : format(values.knownNet),
});
export function calculate(input: any) {
const data = mapping(input, "data");
const asOfText = data.asOf;
const asOf = timestamp(asOfText, "asOf");
const baseCurrency = currency(data.baseCurrency, "baseCurrency");
const previous = priceObservation(data.previousRegularObservation, "previousRegularObservation", baseCurrency);
const last = priceObservation(data.lastRegularObservation, "lastRegularObservation", baseCurrency);
if (previous.trading >= last.trading) throw new RangeError("previousRegularObservation.tradingAt must precede lastRegularObservation.tradingAt");
const event = mapping(data.event, "event");
const eventId = identifier(event.eventId, "event.eventId");
const reason = event.reason;
if (!REASONS.has(reason)) throw new RangeError("event.reason is outside the documented implementation taxonomy");
const effective = timestamp(event.effectiveAt, "event.effectiveAt");
const [, eventAvailable] = sourceTimes(event, "event");
if (last.trading >= effective) throw new RangeError("lastRegularObservation.tradingAt must precede event.effectiveAt");
const proceedsStatus = data.proceedsStatus;
if (!PROCEEDS_STATUSES.has(proceedsStatus)) throw new RangeError("proceedsStatus is invalid");
if (!Array.isArray(data.proceeds) || !Array.isArray(data.deductions)) throw new TypeError("proceeds and deductions must be arrays");
const availability: Array<[string, number]> = [
["previousRegularObservation", previous.available], ["lastRegularObservation", last.available], ["event", eventAvailable],
];
let gross = 0n;
data.proceeds.forEach((rawLeg: any, index: number) => {
const path = `proceeds[${index}]`;
const leg = mapping(rawLeg, path);
if (leg.kind === "cash") {
const [value, times] = moneyValue(leg, path, baseCurrency);
gross += value; availability.push(...times);
} else if (leg.kind === "stock") {
const quantity = decimal(leg.quantityPerOldShare, `${path}.quantityPerOldShare`, true);
identifier(leg.successorSecurityId, `${path}.successorSecurityId`);
const price = decimal(leg.valuationPrice, `${path}.valuationPrice`, true);
const priceCurrency = currency(leg.priceCurrency, `${path}.priceCurrency`);
const valuationAt = timestamp(leg.valuationAt, `${path}.valuationAt`);
const [legObserved, legAvailable] = sourceTimes(leg, path);
if (valuationAt > legObserved) throw new RangeError(`${path}.valuationAt must not follow observedAt`);
const [rate, fxAvailable] = fx(leg.fxToBase, `${path}.fxToBase`, priceCurrency, baseCurrency);
gross += mul(mul(quantity, price), rate);
availability.push([path, legAvailable], [`${path}.fxToBase`, fxAvailable]);
} else throw new RangeError(`${path}.kind must be cash or stock`);
});
let deductionTotal = 0n;
data.deductions.forEach((rawDeduction: any, index: number) => {
const path = `deductions[${index}]`;
const deduction = mapping(rawDeduction, path);
if (!new Set(["fee", "withholding"]).has(deduction.kind)) throw new RangeError(`${path}.kind must be fee or withholding`);
const [value, times] = moneyValue(deduction, path, baseCurrency);
deductionTotal += value; availability.push(...times);
});
if (proceedsStatus === "complete" && data.proceeds.length === 0) throw new RangeError("complete proceedsStatus requires at least one proceeds leg");
if (proceedsStatus === "partial" && data.proceeds.length === 0) throw new RangeError("partial proceedsStatus requires at least one known proceeds leg");
if (new Set(["unknown", "zero_confirmed"]).has(proceedsStatus) && data.proceeds.length) throw new RangeError(`${proceedsStatus} proceedsStatus must not include proceeds legs`);
if (new Set(["unknown", "zero_confirmed"]).has(proceedsStatus) && data.deductions.length) throw new RangeError("deductions require complete or partial proceeds");
if (proceedsStatus === "zero_confirmed") {
const zero = mapping(data.zeroConfirmation, "zeroConfirmation");
text(zero.statement, "zeroConfirmation.statement", 10);
const [, zeroAvailable] = sourceTimes(zero, "zeroConfirmation");
availability.push(["zeroConfirmation", zeroAvailable]);
} else if (data.zeroConfirmation !== undefined && data.zeroConfirmation !== null) throw new RangeError("zeroConfirmation is allowed only for zero_confirmed proceedsStatus");
const regularUnavailable = availability.slice(0, 2).filter(([, available]) => available > asOf).map(([path]) => path);
const lastReturn = regularUnavailable.length ? null : ratio(last.price, previous.price) - SCALE;
const withheld = [...new Set(availability.filter(([, available]) => available > asOf).map(([path]) => path))].sort();
let observed: any;
if (regularUnavailable.length || eventAvailable > asOf) observed = observedResult("not_yet_available", lastReturn);
else if (effective > asOf) observed = observedResult("event_not_effective", lastReturn);
else if (proceedsStatus === "unknown") observed = observedResult("missing_terminal_proceeds", lastReturn);
else if (withheld.length) observed = observedResult("not_yet_available", lastReturn);
else if (proceedsStatus === "partial") {
const knownNet = gross - deductionTotal;
if (knownNet < 0n) throw new RangeError("known deductions cannot exceed known partial proceeds");
observed = observedResult("missing_terminal_proceeds", lastReturn, {
basis: "partial_observed", knownGross: gross, knownDeductions: deductionTotal, knownNet,
});
}
else {
const basis = proceedsStatus === "zero_confirmed" ? "explicit_zero_observed" : "complete_observed";
if (proceedsStatus === "zero_confirmed") { gross = 0n; deductionTotal = 0n; }
const net = gross - deductionTotal;
if (net < 0n) throw new RangeError("deductions cannot exceed gross observed proceeds");
observed = observedResult("complete", lastReturn, {
gross, deductions: deductionTotal, net,
terminalReturn: ratio(net, last.price) - SCALE,
linkedReturn: ratio(net, previous.price) - SCALE,
basis,
});
}
let researchEstimate: any = null;
if (data.researchImputation !== undefined && data.researchImputation !== null) {
const estimate = mapping(data.researchImputation, "researchImputation");
identifier(estimate.methodId, "researchImputation.methodId");
text(estimate.caveat, "researchImputation.caveat", 20);
const estimatedNet = decimal(estimate.estimatedNetValuePerOldShare, "researchImputation.estimatedNetValuePerOldShare");
const estimateCurrency = currency(estimate.currency, "researchImputation.currency");
const estimateEffective = timestamp(estimate.effectiveAt, "researchImputation.effectiveAt");
const [estimateObserved, estimateAvailable] = sourceTimes(estimate, "researchImputation");
if (estimateEffective > estimateObserved) throw new RangeError("researchImputation.effectiveAt must not follow observedAt");
const [estimateRate, estimateFxAvailable] = fx(estimate.fxToBase, "researchImputation.fxToBase", estimateCurrency, baseCurrency);
const estimateWithheld = [previous.available, last.available, eventAvailable, estimateAvailable, estimateFxAvailable].some(moment => moment > asOf) || effective > asOf;
if (estimateWithheld) researchEstimate = { status: "not_yet_available", terminalNetValuePerOldShare: null, terminalReturn: null, linkedReturn: null };
else {
const estimatedBase = mul(estimatedNet, estimateRate);
researchEstimate = {
status: "research_estimate",
terminalNetValuePerOldShare: format(estimatedBase),
terminalReturn: format(ratio(estimatedBase, last.price) - SCALE),
linkedReturn: format(ratio(estimatedBase, previous.price) - SCALE),
};
}
}
const missingEvidence = proceedsStatus === "unknown" ? ["proceeds"] : proceedsStatus === "partial" ? ["proceeds.completeSet"] : [];
const knownProceedsEvidence = [...new Set(availability.slice(3).filter(([path, available]) => available <= asOf && (path.startsWith("proceeds[") || path.startsWith("deductions[") || path === "zeroConfirmation")).map(([path]) => path))].sort();
const eventVisible = eventAvailable <= asOf;
return {
observed,
researchEstimate,
diagnostics: {
eventId: eventVisible ? eventId : null,
eventReason: eventVisible ? reason : null,
asOf: asOfText,
effectiveAt: eventVisible ? event.effectiveAt : null,
lastRegularTradingAt: last.available <= asOf ? data.lastRegularObservation.tradingAt : null,
observedEvidenceAvailable: withheld.length === 0,
missingEvidence,
withheldEvidence: withheld,
knownProceedsEvidence,
baseCurrency,
},
};
}
The embedded lab now expands to its full document height, keeping the article as the only scroll surface.