D02-F02-A03 / Complete engineering topic

Stock-Dividend Adjustment: Convert Price and Quantity Bases Without Inventing Value

A production-minded guide to Stock-Dividend Adjustment: Convert Price and Quantity Bases Without Inventing Value.

D02 · CORPORATE ACTIONS AND SECURIT…
D02-F02-A03Canonical / Tested / Open
D02 / D02-F02
Key concepts

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

A stock dividend can make a healthy security history look broken. The price may step down while shares step up, even though a same-class pro-rata distribution creates no value by itself. The useful result is not merely a smooth chart. It is a traceable series whose price and quantity units mean the same thing on both sides of the event.

This tutorial builds one narrow, testable method: carry prices, share quantities, and optionally share-denominated volume onto a later basis for a confirmed, mandatory distribution of the same security class.

The idea in one picture

If a holder receives rr new shares for every old share, one old share becomes 1+r1+r total shares. A backward series divides old-basis prices and multiplies old-basis quantities by that same factor.

A same-class stock dividend moves price and quantity in reciprocal directions while preserving value

The raw record still matters. Keep it unchanged beside the derived series, source event, revision time, and method name.

Define the rate before calculating

Let:

SymbolMeaningUnit
rrnew same-class shares per old shareshares/share
fftotal post-event shares per old sharefactor
PP^-price on the pre-event basiscurrency/old share
QQ^-comparable share quantity on the pre-event basisold shares
VV^-share-denominated volumeold shares traded

Then:

f=1+rf=1+r

This is the most common input mistake. A 10% stock dividend means r=0.10r=0.10 and f=1.10f=1.10. A four-for-one split carried out as a stock dividend means three new shares per old share, so r=3r=3 and f=4f=4.

For a post-event anchor basis:

Padj=Pf,Qadj=QfP^{adj}=\frac{P^-}{f}, \qquad Q^{adj}=Q^-f

The transformation preserves the analytical value identity:

PQ=PadjQadjP^-Q^-=P^{adj}Q^{adj}

Volume has two defensible meanings. adjust_to_anchor_basis multiplies old share counts by ff so volume uses the later share unit. preserve_reported keeps the number reported by the source. Neither convention is universal; the metadata must say which one was used.

A stock dividend can be split-equivalent

The event label and the scalar arithmetic answer different questions. Legal documents, accounting systems, exchanges, and vendors may call an event a stock dividend, stock split, bonus issue, or scrip issue. For a mandatory pro-rata issue of the same class at no charge, the basis factor is still 1+r1+r.

That equivalence stops when the distributed asset differs, a holder must elect, a subscription price exists, or cash/property enters the terms. Those cases need a distribution or rights method, not this reciprocal factor.

Dates do different jobs

  • Announcement or publication time says when a revision became available.
  • Record date identifies holders for an issuer or agent process.
  • Payment or distribution date says when the additional shares are delivered.
  • Ex-date marks when the security trades without the entitlement and is the series-adjustment boundary.

Do not infer the ex-date from the record date. FINRA's current Rule 11140 distinguishes smaller and larger distributions and separately addresses ADRs and foreign securities. Nasdaq's Daily List format carries ex, record, and payment dates as separate fields. The implementation therefore requires an authoritative exAt instant.

Rendering system map…

The decisive boundary is observation.at < exAt. An observation exactly at exAt is already on the new basis and must not be adjusted twice.

Revisions and no-look-ahead

Each event can carry several revisions. At an asOf time, the algorithm selects the latest uniquely timestamped revision not later than that cutoff. A later correction can restate today's history, but it cannot be used in a simulation of what was known before its publication.

The target anchorAt must also be no later than asOf. Each observation carries its own availableAt and sourceId; a row unavailable at the knowledge cutoff is rejected. This standard mode therefore cannot quietly turn an announced future event into an effective historical adjustment.

The output records:

  • the selected revision publication time;
  • whether that revision was known by the ex-date;
  • the source identifier;
  • cancelled events;
  • events for which no revision was yet available.

Two revisions with the same publication instant are rejected as ambiguous unless an upstream source-priority policy resolves them.

Multiple events compound

For an observation at time tt and a target basis at anchorAt, multiply every eligible factor whose ex instant falls after tt and no later than the anchor:

Ft=j:t<exAtjanchorAt(1+rj)F_t=\prod_{j:t<exAt_j\le anchorAt}(1+r_j)

Then use Pt/FtP_t/F_t for price and QtFtQ_tF_t for shares.

Synthetic worked example

The shared fixture uses two fictional same-class events: 10% and 20%. The earliest observation has a synthetic price of 50, 200 shares, and volume of 80.

F=(1+0.10)(1+0.20)=1.32F=(1+0.10)(1+0.20)=1.32
FieldRawOperationAnchor-basis result
Price50÷1.32\div1.3237.878787878788
Shares200×1.32\times1.32264
Volume80×1.32\times1.32105.6

Two synthetic stock dividends compound into one reciprocal price and quantity bridge

The event at the second boundary has an earlier 15% source revision and a later 20% revision. Move asOf earlier and the implementation selects 15%; move it later and it selects 20%. That is an evidence change, not a formula change.

A researched historical event without invented prices

NVIDIA's SEC-filed announcement described its 2021 four-for-one split as a stock dividend. The issuer stated that holders would receive three additional common shares for each old share, used a 2021-06-21 record date, distributed the shares after the 2021-07-19 close, and expected split-adjusted trading on 2021-07-20. NVIDIA's FAQ identifies July 20 as the ex-dividend date, explains the due-bill period, and says NVIDIA itself would not issue fractional shares.

Those are sourced issuer facts. The only derived arithmetic here is:

r=3,f=1+3=4,1/f=0.25r=3, \qquad f=1+3=4, \qquad 1/f=0.25

No NVIDIA price, volume, cash-in-lieu amount, tax result, or provider-adjusted observation appears in this package. Adding one would require an authoritative observation, exact timestamp/session semantics, a named provider methodology and version, revision provenance, and redistribution permission. This restraint is part of the method, not a missing flourish.

Fractional shares are a settlement problem

An analytical series can preserve fractional units. Holder settlement may not. For HH held shares, the exact new entitlement is N=HrN=Hr. If a supplied policy delivers only increments of uu:

Ndelivered=uNuN_{delivered}=u\left\lfloor\frac{N}{u}\right\rfloor

The remainder is not automatically cash. The helper returns pending_cash_price until the caller supplies a price and currency. It applies taxes and fees only when the scenario explicitly provides them. Its binary-float reference calculation floors without an upward tolerance, so delivered shares never exceed the exact binary entitlement; production settlement should use the contractual decimal ratio and increment. NVIDIA's FAQ, for example, tells fractional-share traders to consult their broker; it does not support inventing one universal settlement amount.

Implementation walkthrough

The Python and TypeScript versions use the same sequence:

  1. validate the method, identifiers, timestamps, dates, anchorAt <= asOf, observation availability/source lineage, order, and numeric domains;
  2. select revisions using publishedAt <= asOf;
  3. remove cancellations and report unavailable events;
  4. reject a distribution into a different security class;
  5. compound only events effective by the anchor and after each observation;
  6. transform price and quantities in reciprocal directions;
  7. round only the public output with nearest, ties away from zero, and retain audit fields.

Both languages reconstruct the exact integer ratio of each IEEE-754 value before 12-decimal integer rounding. This avoids Python/JavaScript tie-rule drift, including values whose scaled integer exceeds JavaScript's safe-integer boundary.

Both consume the same fixture. Their exact output must match.

Explore the event boundary

Open the guided stock-dividend lab. It contains 36 synthetic observations and three scenarios:

  • canonical 10% same-class event;
  • two-event compounding;
  • wrong-class rejection.

Use Back and Step to cross the ex-date one state at a time. Switch the volume policy to see why a price-adjusted chart does not answer whether historical volume was normalized. Reset always restores the same canonical state.

Failure modes worth making loud

Scope and evidence guards stop invalid stock-dividend adjustments before arithmetic

  • Ratio confusion: passing 4 as a new-shares rate produces a factor of 5, not a four-for-one event.
  • Wrong class: a dividend payable in another company is not a same-line quantity change.
  • Date substitution: record and payment dates are evidence, not automatic ex-date replacements.
  • Hidden volume convention: reported and basis-normalized volume are different series.
  • Revision leakage: today's corrected rate was not necessarily known on the historical decision date.
  • Invented fractional cash: no price means a pending settlement, not zero and not an estimate.
  • Rounding too early: compounding rounded event factors can drift from the declared ratio.
  • Overclaiming provider parity: a matching chart cannot establish parity without the provider's method and raw records.

Provider methods are product-specific

FTSE Russell's June 2026 guide gives a same-stock scrip price factor as shares before divided by shares after and applies it on the ex-date. Its one-for-one scrip example is equivalent to a two-for-one split. S&P DJI explains that corporate-action treatment differs across market-cap, price-weighted, equal-weighted, and other index categories. Nasdaq's specification distinguishes a Stock Dividend or Split from stock payable in another company.

These sources support the method's explicit choices; they do not make this reference implementation a replica of an index or vendor product.

What you can now verify

You can translate a same-class stock-dividend rate, place it at the right boundary, select revisions without look-ahead, compound events, choose a volume convention, and keep fractional settlement pending until evidence exists. Continue with Special-Dividend Adjustment, where cash value and return methodology become central.

Asset map

AssetTeaching roleVideo sceneSourceStatic fallback
Reciprocal basis bridgeIntuition2-3visuals/static/article-hero.svgSame SVG
Revision and boundary flowAlgorithm4visuals/mermaid/calculation-flow.mdText algorithm
Compounded fixtureArithmetic5visuals/static/worked-example.svgWorked table
Guided labScenarios4-7visuals/animated/playground.htmlWorked SVG
Scope guardsFailures8visuals/static/failure-guard.svgGuard list

Primary source roles, dates, jurisdictions, and limitations are recorded in ../REFERENCES.md. This tutorial is educational, not investment, legal, accounting, tax, or settlement advice.

Revision selection and adjustment flow

The first decision is what evidence existed by asOf; the second is which observations precede exAt.

Rendering system map…

Takeaway: revision availability and event effectiveness are independent gates.

Event evidence lifecycle

This state model preserves late revision and cancellation instead of overwriting the source history.

Rendering system map…

Takeaway: a later revision can change a current restatement, but it cannot be used at an earlier knowledge cutoff.

References6 primary sources and evidence notes

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

All sources were accessed on 2026-07-22. The package redistributes no issuer, exchange, index-provider, or market-data records. Its datasets are synthetic.

R1 - NVIDIA 2021 stock-split announcement filed with the SEC

  • Organization or authors: NVIDIA Corporation
  • Source type: Issuer press release filed as Exhibit 99.1 to SEC Form 8-K
  • Publication date: 2021-05-21
  • URL: https://www.sec.gov/Archives/edgar/data/1045810/000104581021000056/pr-may2021.htm
  • Jurisdiction: United States; NVIDIA common stock
  • Supports: The board declared a four-for-one split in the form of a stock dividend; the announced terms were three additional common shares for each share, record date 2021-06-21, distribution after the 2021-07-19 close, and expected split-adjusted trading on 2021-07-20.
  • Limitations: The release was conditional on stockholder approval when filed. It supplies no market price, adjusted-volume series, or third-party provider observation.

R2 - NVIDIA 2021 Stock Split FAQ

  • Organization or authors: NVIDIA Corporation
  • Source type: Official issuer investor FAQ
  • Publication date: 2021
  • URL: https://investor.nvidia.com/files/doc_downloads/doc_faq/06/21/NVIDIA-2021-Stock-Split-FAQ.pdf
  • Jurisdiction: NVIDIA common stock; tax comments expressly limited by the issuer
  • Supports: Record, distribution, and ex-dividend dates; three additional shares per old share; due-bill treatment for trading between record and distribution dates; issuer statement that it would not issue fractional shares; instruction to consult brokers for fractional-share handling.
  • Limitations: It is not a universal settlement, tax, accounting, or data-vendor methodology. The package does not infer cash-in-lieu terms that the FAQ does not provide.

R3 - FINRA Rule 11140

  • Organization or authors: Financial Industry Regulatory Authority
  • Source type: Current self-regulatory organization rule
  • Effective version: Amended for T+1 effective 2024-05-28
  • URL: https://www.finra.org/rules-guidance/rulebooks/finra-rules/11140
  • Jurisdiction: Securities within the rule's scope; exchange-listed securities can have an exchange-designated ex-date
  • Supports: Ex-date designation is rule- and market-specific. Current rule text distinguishes distributions below 25% from stock dividends or splits at 25% or greater and separately addresses ADRs and foreign securities.
  • Limitations: The reference implementation does not calculate an ex-date from the rule. It requires an authoritative exAt instant.

R4 - Nasdaq Daily List File Format and Specifications

  • Organization or authors: Nasdaq
  • Source type: Official exchange data-product specification
  • Version: Accessed current edition
  • URL: https://www.nasdaqtrader.com/content/technicalsupport/specifications/dataproducts/dlcompletespec.pdf
  • Jurisdiction: Nasdaq Daily List products
  • Supports: Dividend records carry declaration, ex, record, and payment dates; the specification distinguishes Stock Dividend or Split (XS) and Stock Dividend payable in another company (CP), demonstrating why event type and distributed security must be explicit.
  • Limitations: Historical records and some fields are subscription products. This package uses no Daily List observation and claims no vendor parity.

R5 - FTSE Russell Corporate Actions and Events Guide for Market Capitalisation Weighted Indices

  • Organization or authors: FTSE Russell, LSEG
  • Source type: Official index-provider methodology
  • Publication date and version: June 2026, v6.9
  • URL: https://www.lseg.com/content/dam/ftse-russell/en_us/documents/policy-documents/corporate-actions-and-events-guide.pdf
  • Jurisdiction: Index series whose methodology incorporates the guide
  • Supports: Section 4.7 defines same-stock scrip issues as pro-rata shares at no charge and gives the price factor as shares held before divided by shares held after, applied on the ex-date. Its one-for-one example is equivalent to a two-for-one split.
  • Limitations: This is an index methodology, not a universal raw-market-data rule. It also treats different-stock distributions separately, which the reference implementation rejects as out of scope.

R6 - S&P DJI Index Mathematics Methodology

  • Organization or authors: S&P Dow Jones Indices
  • Source type: Official index-provider methodology
  • Publication date: 2026 accessed edition
  • URL: https://www.spglobal.com/spdji/en/documents/methodologies/methodology-index-math.pdf
  • Jurisdiction: S&P DJI index families subject to their own methodologies
  • Supports: Corporate-action treatment varies by index category; price-weighted, equal-weighted, and market-cap-weighted indices can require different maintenance. The divisor maintains continuity for non-market changes.
  • Limitations: It does not define this package's security-level historical-volume convention. Users must follow the methodology of the specific product they reproduce.

Evidence classification

  • Sourced historical facts: NVIDIA event terms and dates stated in R1-R2.
  • Derived arithmetic: three new shares per old share means a share factor of 1+3=41+3=4 and a reciprocal price factor of 1/4=0.251/4=0.25.
  • Synthetic teaching data: every price, share count, volume, revision, fee, tax rate, and cash-in-lieu price in fixtures, charts, examples, and the lab.
  • Deliberately not claimed: an observed NVIDIA market price, actual broker fractional settlement, actual adjusted volume, or parity with any licensed provider.
stock_dividend_adjustment.ts
/** Auditable same-class stock-dividend adjustment with as-of revision selection. */
const OUTPUT_SCALE = 1000000000000n;
const exactRatio = (value) => {
  const bytes = new ArrayBuffer(8), view = new DataView(bytes); view.setFloat64(0,value,false); const bits=view.getBigUint64(0,false);
  const negative=(bits>>63n)===1n, exponent=Number((bits>>52n)&0x7ffn), fraction=bits&((1n<<52n)-1n);
  if(exponent===0x7ff)throw new TypeError("output must be finite");
  if(exponent===0&&fraction===0n)return {negative,numerator:0n,denominator:1n};
  const mantissa=exponent===0?fraction:(1n<<52n)|fraction, power=(exponent===0?1-1023:exponent-1023)-52;
  return power>=0?{negative,numerator:mantissa<<BigInt(power),denominator:1n}:{negative,numerator:mantissa,denominator:1n<<BigInt(-power)};
};
export const roundOutput = (value) => { numberValue(value,"output"); const {negative,numerator,denominator}=exactRatio(value);let quotient=(numerator*OUTPUT_SCALE)/denominator;const remainder=(numerator*OUTPUT_SCALE)%denominator;if(remainder*2n>=denominator)quotient+=1n;const result=Number(quotient)/1e12;return negative?-result:result; };
const rounded = roundOutput;
const numberValue = (value, name, minimum = null, strict = false) => {
  if (typeof value !== "number" || !Number.isFinite(value)) throw new TypeError(`${name} must be a finite number`);
  if (minimum !== null && (strict ? value <= minimum : value < minimum)) throw new RangeError(`${name} is outside its allowed range`);
  return value;
};
const textValue = (value, name) => { if (typeof value !== "string" || !value.trim()) throw new TypeError(`${name} must be a non-empty string`); return value; };
const instant = (value, name) => {
  const text = textValue(value, name);
  if (!/(Z|[+-]\d\d:\d\d)$/.test(text) || Number.isNaN(Date.parse(text))) throw new TypeError(`${name} must be an ISO 8601 instant with offset`);
  return new Date(text).getTime();
};
const dateValue = (value, name) => { const text = textValue(value, name), parsed=new Date(`${text}T00:00:00Z`); if (!/^\d{4}-\d{2}-\d{2}$/.test(text) || Number.isNaN(parsed.getTime()) || parsed.toISOString().slice(0,10)!==text) throw new TypeError(`${name} must be YYYY-MM-DD`); return text; };

function selectEvent(event, asOf, securityId) {
  const eventId = textValue(event.eventId, "eventId");
  const subject = textValue(event.subjectSecurityId, `${eventId}.subjectSecurityId`);
  const distributed = textValue(event.distributedSecurityId, `${eventId}.distributedSecurityId`);
  if (subject !== securityId) throw new RangeError(`${eventId} does not apply to securityId`);
  if (distributed !== subject) throw new RangeError(`${eventId} distributes another security; use a distribution method`);
  const exAtText = textValue(event.exAt, `${eventId}.exAt`), exAt = instant(exAtText, `${eventId}.exAt`);
  dateValue(event.recordDate, `${eventId}.recordDate`); dateValue(event.payableDate, `${eventId}.payableDate`);
  if (!Array.isArray(event.revisions) || !event.revisions.length) throw new TypeError(`${eventId}.revisions must be a non-empty list`);
  const seen = new Set(), available = [];
  for (const revision of event.revisions) {
    if (!revision || typeof revision !== "object" || Array.isArray(revision)) throw new TypeError(`${eventId} revision must be an object`);
    const published = instant(revision.publishedAt, `${eventId}.publishedAt`);
    if (seen.has(published)) throw new RangeError(`${eventId} has ambiguous revisions at one publishedAt`);
    seen.add(published); if (published <= asOf) available.push({published, revision});
  }
  if (!available.length) return null;
  available.sort((a,b) => a.published - b.published); const {published, revision} = available.at(-1);
  if (!["confirmed","cancelled"].includes(revision.status)) throw new RangeError(`${eventId}.status must be confirmed or cancelled`);
  const sourceId = textValue(revision.sourceId, `${eventId}.sourceId`);
  if (revision.status === "cancelled") return {cancelled:true,eventId,published};
  const rate = numberValue(revision.rateNewSharesPerOld, `${eventId}.rateNewSharesPerOld`, 0, true), factor = 1 + rate;
  if (!Number.isFinite(factor)) throw new RangeError(`${eventId} factor is not finite`);
  return {cancelled:false,eventId,exAt,exAtText,published,publishedText:revision.publishedAt,rate,factor,sourceId};
}

export function calculate(data) {
  if (!data || typeof data !== "object" || Array.isArray(data)) throw new TypeError("data must be an object");
  const payload = structuredClone(data);
  if (payload.method !== "backward_post_event_same_class") throw new RangeError("unsupported method");
  const securityId = textValue(payload.securityId,"securityId"), asOf = instant(payload.asOf,"asOf"), anchorAt = instant(payload.anchorAt,"anchorAt");
  if(anchorAt>asOf)throw new RangeError("anchorAt must not be later than asOf");
  if (!["adjust_to_anchor_basis","preserve_reported"].includes(payload.volumePolicy)) throw new RangeError("invalid volumePolicy");
  if (!Array.isArray(payload.events)) throw new TypeError("events must be a list");
  const selected = [], unavailable = [], cancelled = [], ids = new Set();
  for (const event of payload.events) {
    if (!event || typeof event !== "object" || Array.isArray(event)) throw new TypeError("each event must be an object");
    const eventId = textValue(event.eventId,"eventId"); if (ids.has(eventId)) throw new RangeError("eventId must be unique"); ids.add(eventId);
    const chosen = selectEvent(event,asOf,securityId);
    if (!chosen) unavailable.push(eventId); else if (chosen.cancelled) cancelled.push(eventId); else if (chosen.exAt <= anchorAt) selected.push(chosen);
  }
  selected.sort((a,b) => a.exAt-b.exAt || a.eventId.localeCompare(b.eventId));
  if (!Array.isArray(payload.observations) || !payload.observations.length) throw new TypeError("observations must be a non-empty list");
  let previousAt = null;
  const rows = payload.observations.map(observation => {
    if (!observation || typeof observation !== "object" || Array.isArray(observation)) throw new TypeError("each observation must be an object");
    const atText = textValue(observation.at,"observation.at"), at = instant(atText,"observation.at");
    const availableAtText=textValue(observation.availableAt,"observation.availableAt"),availableAt=instant(availableAtText,"observation.availableAt"),sourceId=textValue(observation.sourceId,"observation.sourceId");
    if(availableAt<at)throw new RangeError("observation.availableAt must not precede observation.at");if(availableAt>asOf)throw new RangeError("observation was not available by asOf");
    if (previousAt !== null && at <= previousAt) throw new RangeError("observations must be strictly increasing"); if (at > anchorAt) throw new RangeError("observation occurs after anchorAt"); previousAt = at;
    const rawPrice=numberValue(observation.price,"price",0,true),rawShares=numberValue(observation.shares,"shares",0),rawVolume=numberValue(observation.volume,"volume",0);
    const applicable=selected.filter(event=>at<event.exAt); let factor=1; for(const event of applicable){factor*=event.factor;if(!Number.isFinite(factor))throw new RangeError("cumulative factor is not finite");}
    return {at:atText,availableAt:availableAtText,sourceId,rawPrice:rounded(rawPrice),adjustedPrice:rounded(rawPrice/factor),rawShares:rounded(rawShares),adjustedShares:rounded(rawShares*factor),rawVolume:rounded(rawVolume),adjustedVolume:rounded(payload.volumePolicy==="adjust_to_anchor_basis"?rawVolume*factor:rawVolume),cumulativeShareFactor:rounded(factor),appliedEventIds:applicable.map(event=>event.eventId)};
  });
  const appliedEvents=selected.map(event=>({eventId:event.eventId,exAt:event.exAtText,revisionPublishedAt:event.publishedText,knownByExDate:event.published<=event.exAt,rateNewSharesPerOld:rounded(event.rate),shareFactor:rounded(event.factor),priceFactor:rounded(1/event.factor),sourceId:event.sourceId}));
  return {method:payload.method,securityId,asOf:payload.asOf,anchorAt:payload.anchorAt,volumePolicy:payload.volumePolicy,appliedEvents,unavailableEventIds:unavailable,cancelledEventIds:cancelled,rows};
}

export function settleEntitlement(data) {
  if (!data || typeof data !== "object" || Array.isArray(data)) throw new TypeError("data must be an object");
  const held=numberValue(data.heldShares,"heldShares",0),rate=numberValue(data.rateNewSharesPerOld,"rateNewSharesPerOld",0,true),increment=numberValue(data.shareIncrement??1,"shareIncrement",0,true),policy=data.fractionalPolicy;
  if (!["preserve_fractional","cash_in_lieu"].includes(policy)) throw new RangeError("invalid fractionalPolicy");
  const exact=held*rate;
  if(policy==="preserve_fractional")return {status:"settled",exactNewShares:rounded(exact),deliveredShares:rounded(exact),fractionalShares:0,grossCash:0,netCash:0,currency:null};
  const delivered=Math.floor(exact/increment)*increment;if(delivered>exact)throw new RangeError("fractional policy would over-deliver shares");const fractional=Math.max(0,exact-delivered);
  if(fractional<=1e-12)return {status:"settled",exactNewShares:rounded(exact),deliveredShares:rounded(delivered),fractionalShares:0,grossCash:0,netCash:0,currency:null};
  if(data.cashInLieuPrice==null)return {status:"pending_cash_price",exactNewShares:rounded(exact),deliveredShares:rounded(delivered),fractionalShares:rounded(fractional),grossCash:null,netCash:null,currency:null};
  const price=numberValue(data.cashInLieuPrice,"cashInLieuPrice",0,true),currency=textValue(data.currency,"currency"),withholding=numberValue(data.withholdingRate??0,"withholdingRate",0),fees=numberValue(data.fees??0,"fees",0);
  if(withholding>1)throw new RangeError("withholdingRate must not exceed 1"); const gross=fractional*price,net=gross*(1-withholding)-fees;if(net < -1e-12)throw new RangeError("fees and withholding exceed gross cash");
  return {status:"settled",exactNewShares:rounded(exact),deliveredShares:rounded(delivered),fractionalShares:rounded(fractional),grossCash:rounded(gross),netCash:rounded(Math.max(0,net)),currency};
}
Full-height labplaygroundOpen full screen