D02-F02-A01 / Complete engineering topic

Rights-Issue TERP Adjustment

Calculate TERP without confusing a methodology reference price with a traded ex-rights price.

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

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

A rights issue can create an apparent price drop even when an eligible holder receives something valuable: the right to buy new shares at a stated subscription price. A clean history needs to represent that transfer without inventing a market price.

This tutorial gives you an auditable calculation. You will build TERP, value the right in two different units, handle currencies and event revisions, and test the result against a real issuer-documented event.

A rights issue separates a sourced event, a reference-price convention, and the market outcome

Start with the distinction that prevents most errors

Theoretical ex-rights price, or TERP, is a reference calculation. It combines an old-share price reference with the cash paid for new shares. It is not a prediction of the opening trade, and it is not evidence that any share changed hands at that number.

Keep these three layers separate:

  1. Sourced facts: offer ratio, subscription price, currencies, issuer dates, reference-price source, revisions.
  2. Methodology choices: which price and time to use, whether to remove a dividend disadvantage, which FX observation and out-of-money policy apply.
  3. Derived values: TERP, implied rights values, adjustment factor, and fractional entitlement.

Observed ex-rights trades form a fourth layer. They need market data and are outside this algorithm.

Read the entitlement basis literally

"7 new shares for every 24 existing shares" means:

  • n=24n=24 existing shares;
  • m=7m=7 new shares;
  • each existing share carries m/n=7/24m/n=7/24 of an entitlement to one new share.

Do not invert that ratio. Do not call PTERPP-TERP "the value of one right" unless you specify that it is value per old share. The right to acquire one whole new share has a different value.

Put prices on one basis

Let PrawP_{raw} be the selected cum-rights market reference and DD a cash or dividend disadvantage removed by the chosen methodology:

P=PrawDP=P_{raw}-D

Let SS be the subscription price, XX the number of quote-currency units per subscription-currency unit, and FF and TT any explicitly included fee and tax per new share:

C=SX+F+TC=SX+F+T

If both prices are in pence, keep both in pence. If one says GBP and the other says GBp, convert the scale before calculation. For different currencies, store the FX source and the time it became available. A later FX fixing cannot enter an earlier decision.

Calculate TERP and both rights values

For an in-the-money issue where C<PC<P:

TERP=nP+mCn+mTERP=\frac{nP+mC}{n+m}

The value theoretically removed from one old share is:

Vold=PTERP=m(PC)n+mV_{old}=P-TERP=\frac{m(P-C)}{n+m}

The value of the entitlement to buy one new share is:

Vright=TERPC=n(PC)n+mV_{right}=TERP-C=\frac{n(P-C)}{n+m}

These agree after scaling: Vold=(m/n)VrightV_{old}=(m/n)V_{right}. The analytical backward-adjustment factor is A=TERP/PA=TERP/P.

SymbolMeaningUnit
nn, mmExisting and new shares in the offer basisshares
PrawP_{raw}Sourced cum-rights price referencequote currency/old share
DDDeclared cash or dividend disadvantagequote currency/old share
PPConvention-adjusted reference pricequote currency/old share
SSSubscription pricesubscription currency/new share
XXQuote currency per subscription currencyFX rate
FF, TTIncluded fee and tax per new sharequote currency/new share
CCSubscription cash equivalentquote currency/new share
TERPTERPTheoretical ex-rights pricequote currency/enlarged-basis share
VoldV_{old}Implied right value per old sharequote currency/old share
VrightV_{right}Implied value per right to one new sharequote currency/right

A real event: National Grid's 2024 rights issue

National Grid's 23 May 2024 prospectus states the following facts:

  • 7 new shares for every 24 existing shares;
  • subscription price of 645 pence per new share;
  • entitlement record time of 18:00 London time on 20 May;
  • existing shares ex-rights and nil-paid dealings starting at 08:00 on 24 May;
  • final acceptance, payment, and registration-of-renunciation deadline at 11:00 on 10 June;
  • fully paid new-share dealings starting at 08:00 on 12 June.

The issuer also identified a 1,127.5 GBp closing middle-market price on 22 May and a 39.12 GBp final dividend not payable on the new shares. Under the declared dividend-adjusted reference convention:

P=1127.539.12=1088.38 GBpP=1127.5-39.12=1088.38\ \text{GBp}

Now derive TERP:

TERP=24(1088.38)+7(645)31=988.261935 GBpTERP=\frac{24(1088.38)+7(645)}{31}=988.261935\ \text{GBp}

That gives:

  • Vold=100.118065V_{old}=100.118065 GBp per old share;
  • Vright=343.261935V_{right}=343.261935 GBp per right to acquire one new share;
  • A=0.908012A=0.908012;
  • issue-price discount to TERP of 34.7339%.

The last number reconciles to National Grid's rounded 34.7% statement. It still does not tell us what National Grid traded at on 24 May.

The National Grid terms form a value-preserving bridge to a theoretical reference, not an observed trade

Event dates are different clocks

Rendering system map…

National Grid's record date preceded the announcement. That is not permission to use the terms before publication. A point-in-time system stores both the event's legal/market time and the time the evidence became available to the system.

The package knows the prospectus publication day but does not invent a publication second. Its historical fixture uses 23:59:59Z as a conservative end-of-day availability convention and labels that as an engineering policy.

Revisions matter too. A later corrected ratio or cancellation must create a new revision. The implementation selects the latest availableAt <= asOf; if nothing was available, it stops.

Renunciation, nil-paid rights, and oversubscription

A nil-paid right is an entitlement before the subscription price has been paid. When rights are renounceable, that entitlement can be transferred or sold subject to the offer, venue, broker, and jurisdiction rules. Its traded price can differ from VrightV_{right}.

Oversubscription is another operation: a holder asks for more than the base entitlement, and an allocation policy decides what they receive. It does not change nn, mm, or TERP. The National Grid teaching extract does not establish an oversubscription facility, so the data says "not documented" instead of assuming yes or no.

Fractional entitlements also belong to the legal terms. For 100 old shares, the exact entitlement is:

100(724)=29.166667100\left(\frac{7}{24}\right)=29.166667

The calculator reports 29 whole shares plus a 0.166667 fraction. National Grid's prospectus says entitlements were rounded down and fractions generally aggregated for the company's benefit. Another offer can have another policy.

Fees, taxes, and issuer expenses

National Grid estimated issuer expenses but also stated that the company charged investors no commissions, fees, or expenses in connection with the rights issue. Those are different quantities. Do not allocate issuer expenses into a shareholder TERP formula unless the chosen methodology explicitly says to do so.

Taxes depend on holder and jurisdiction. The calculator supports explicit per-new-share amounts only when a documented analytical convention requires them. Zero means "not included in this calculation," not "the investor has no tax cost."

Reject plausible-looking nonsense

If CPC \ge P, the algebra produces a zero or negative rights value. Many production methodologies make no price adjustment; others delay treatment. The reference implementation requires the caller to choose:

  • reject: stop and request the correct methodology;
  • no_adjustment: return TERP=PTERP=P and zero rights value, with applied=false.

It never publishes a negative right value.

Invalid denominators, unavailable evidence, and out-of-money rights stop before a reference adjustment

The implementation also rejects zero or fractional offer bases, a cash disadvantage that removes the entire reference price, same-currency FX other than one, future FX evidence, timeline inversions, cancellation, and missing source revisions.

Implement it causally

The Python and TypeScript versions follow the same sequence:

  1. select the knowable revision;
  2. validate the timeline and terms;
  3. normalize price, cash, currency, and FX;
  4. apply the out-of-money policy;
  5. calculate without intermediate rounding;
  6. emit revision identifiers and six-decimal diagnostics.

The shared tests cover 16 cases in each language, including cross-currency conversion, future FX, fractional entitlement, cancellation, out-of-money handling, and input immutability.

Explore the reference convention

Open the guided TERP lab. Step through evidence, normalization, calculation, rights values, and the operational timeline. Then compare a cross-currency case and an out-of-money rejection. Reset is deterministic, and the lab's historical values match the shared fixture.

What this result can and cannot prove

Passing the tests proves that the implementation matches its declared convention for covered cases. It does not prove that a specific index provider, broker, tax authority, or data vendor would use the same price, FX, timeline, rounding, or event treatment. It also does not prove an observed ex-rights price or an investment outcome.

Summary

TERP is valuable because it makes an adjustment explainable, not because it predicts a trade. Preserve the old/new basis, source price and timing, currency and FX, issuer timeline, revision history, and policy choices. Calculate both rights-value units. Keep allocation and legal terms beside the arithmetic, and stop when evidence is not knowable.

Continue with Spin-Off Price Adjustment. For earnings comparability, see Rights-Issue Bonus-Factor Adjustment.

Asset map

AssetTeachesSource
Evidence-to-reference overviewFact, convention, derivation, market separationvisuals/static/article-hero.svg
National Grid value bridgeExact historical derivationvisuals/static/worked-example.svg
Failure guardInvalid and out-of-money behaviorvisuals/static/failure-guard.svg
Guided labStepwise scenarios and timelinevisuals/animated/playground.html

Primary source roles and limitations are in REFERENCES.md. This is educational material, not investment, legal, accounting, or tax advice.

TERP evidence and calculation flow

This flow separates sourced evidence from normalization, policy, and derived output.

Rendering system map…

Takeaway: a reproducible number begins with evidence that was knowable at the decision time; TERP remains a theoretical reference.

Rights-issue operational lifecycle

The National Grid case shows why one generic "event date" is not enough.

Rendering system map…

Takeaway: record, availability, ex-rights, nil-paid, payment, and fully paid states answer different questions and may use different clocks.

References6 primary sources and evidence notes

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

Accessed 2026-07-22. No prospectus, exchange feed, index data, or vendor data is redistributed in this package.

R1 - National Grid plc Rights Issue Prospectus

  • Organization: National Grid plc; approved by the UK Financial Conduct Authority as described in the document
  • Source type: Issuer prospectus
  • Publication date: 2024-05-23
  • URL: https://www.nationalgrid.com/document/152061/download
  • Jurisdiction: United Kingdom
  • Supports: 7-for-24 basis; 645 GBp subscription price; record, ex-rights, nil-paid dealing, payment, renunciation, and fully paid dealing dates; fractional treatment; nil-paid and fully paid definitions; investor-fee and tax cautions
  • Limitations: The timetable was stated as indicative and channel-specific deadlines differed. FCA approval is not an endorsement of the issuer or securities.

R2 - National Grid 7-for-24 Rights Issue announcement

  • Organization: National Grid plc
  • Source type: Official issuer announcement
  • Publication date: 2024-05-23
  • URL: https://www.nationalgrid.com/document/152016/download
  • Jurisdiction: United Kingdom
  • Supports: Offer size and terms; 1,127.5 GBp closing middle-market reference on 2024-05-22; 39.12 GBp dividend disadvantage; issuer's rounded 34.7% issue-price discount to TERP
  • Limitations: The announcement says investment decisions must be based on the prospectus. It does not provide an observed ex-rights trade for this tutorial.

R3 - National Grid Rights Issue and Prospectus hub

  • Organization: National Grid plc
  • Source type: Official issuer document hub
  • URL: https://www.nationalgrid.com/gridforgrowth/rights-issue
  • Jurisdiction: United Kingdom
  • Supports: Provenance and current official links for the prospectus, announcement, and admission notice
  • Limitations: A landing page is not the calculation methodology.

R4 - S&P DJI Equity Indices Policies & Practices

  • Organization: S&P Dow Jones Indices
  • Source type: Official index methodology
  • Version: March 2026
  • URL: https://www.spglobal.com/spdji/en/documents/methodologies/methodology-sp-equity-indices-policies-practices.pdf
  • Jurisdiction: Global index methodology
  • Supports: Previous-close cum-rights convention; rights-value, price-adjustment-factor, and TERP relationships; dividend disadvantage; renounceable/non-renounceable terminology; generic rights lifecycle; unknown-price and open-offer exceptions
  • Limitations: S&P DJI policy is not a universal security-master, investor, or tax rule. Terms of use govern index content and data.

R5 - FTSE Russell Corporate Actions and Events Guide

  • Organization: FTSE Russell, LSEG
  • Source type: Official index methodology
  • Version: 6.9, June 2026
  • URL: https://research.ftserussell.com/products/downloads/Corporate_Actions_and_Events_Guide.pdf
  • Jurisdiction: Global index methodology
  • Supports: Standard TERP and price-adjustment-factor example; estimated-price case; nil-paid line treatment; highly dilutive and dividend-disadvantage variants; cancellations and timing exceptions
  • Limitations: FTSE Russell policy is provider-specific and may change. Production use must use the methodology effective for the relevant index and date.

R6 - RFC 3339: Date and Time on the Internet

  • Organization: IETF
  • Source type: Internet standard
  • Publication date: 2002-07
  • URL: https://www.rfc-editor.org/rfc/rfc3339
  • Supports: Offset-bearing timestamp representation used by the package
  • Limitations: Updated by RFC 9557; market-calendar and session semantics remain separate.

Evidence decision

The National Grid case is a historical issuer-documented event. Sourced facts are copied only as compact factual inputs. The 1,088.38 GBp reference, 988.261935 GBp TERP, two rights values, factor, and fractional diagnostic are package derivations. No data-provider observation was needed, and no claim is made about the observed 24 May 2024 ex-rights price.

rights_issue_terp_adjustment.ts
/** Auditable TERP calculator. The output is a convention, not a traded-price claim. */
export type RightsInput = { asOf: string; revisions: RightsRevision[] };
export type RightsRevision = {
  eventId: string; revisionId: string; revisionSequence: number;
  availableAt: string; status: "confirmed" | "cancelled";
  recordAt: string; exAt: string; rightsTradingStartAt: string;
  paymentDeadlineAt: string; newSharesTradeAt: string;
  terms: Record<string, unknown>;
};

const finite = (value: unknown, name: string, minimum?: number): number => {
  if (typeof value !== "number" || !Number.isFinite(value)) throw new TypeError(`${name} must be a finite number`);
  if (minimum !== undefined && value < minimum) throw new RangeError(`${name} must be at least ${minimum}`);
  return value;
};
const positive = (value: unknown, name: string): number => {
  const result = finite(value, name); if (result <= 0) throw new RangeError(`${name} must be positive`); return result;
};
const positiveInteger = (value: unknown, name: string): number => {
  const result = positive(value, name); if (!Number.isInteger(result)) throw new RangeError(`${name} must be a positive integer entitlement basis`); return result;
};
const text = (value: unknown, name: string): string => {
  if (typeof value !== "string" || !value.trim()) throw new TypeError(`${name} must be a non-empty string`); return value;
};
const instant = (value: unknown, name: string): number => {
  const raw = text(value, name);
  if (!/(Z|[+-]\d{2}:\d{2})$/.test(raw)) throw new RangeError(`${name} must include a UTC offset`);
  const result = Date.parse(raw); if (!Number.isFinite(result)) throw new RangeError(`${name} must be an ISO 8601 instant`); return result;
};
const round6 = (value: number): number => Number((Math.round((value + Number.EPSILON) * 1e6) / 1e6).toFixed(6));

function selectRevision(revisions: RightsRevision[], asOf: number): RightsRevision {
  if (!Array.isArray(revisions) || !revisions.length) throw new TypeError("revisions must be a non-empty list");
  let eventId: string | undefined; const seen = new Set<string>();
  const candidates: { availableAt: number; revision: RightsRevision }[] = [];
  for (const revision of revisions) {
    if (!revision || typeof revision !== "object" || Array.isArray(revision)) throw new TypeError("each revision must be an object");
    const currentEventId = text(revision.eventId, "eventId");
    if (eventId === undefined) eventId = currentEventId; else if (currentEventId !== eventId) throw new RangeError("all revisions must describe the same eventId");
    if (!Number.isInteger(revision.revisionSequence) || revision.revisionSequence < 1) throw new RangeError("revisionSequence must be a positive integer");
    const identity = `${text(revision.revisionId, "revisionId")}\u0000${revision.revisionSequence}`;
    if (seen.has(identity)) throw new RangeError("duplicate revision identity"); seen.add(identity);
    const availableAt = instant(revision.availableAt, "availableAt");
    if (availableAt <= asOf) candidates.push({availableAt, revision});
  }
  if (!candidates.length) throw new RangeError("no event revision was available at asOf");
  candidates.sort((a,b) => a.availableAt - b.availableAt || a.revision.revisionSequence - b.revision.revisionSequence);
  return candidates.at(-1)!.revision;
}

function validateTimeline(event: RightsRevision): void {
  const values = [instant(event.recordAt,"recordAt"), instant(event.exAt,"exAt"), instant(event.rightsTradingStartAt,"rightsTradingStartAt"), instant(event.paymentDeadlineAt,"paymentDeadlineAt"), instant(event.newSharesTradeAt,"newSharesTradeAt")];
  if (!(values[0] < values[1] && values[1] <= values[2] && values[2] <= values[3] && values[3] <= values[4])) throw new RangeError("event timeline must satisfy record < ex <= nil-paid trading start <= payment deadline <= new shares");
}

export function calculate(input: RightsInput): Record<string, unknown> {
  if (!input || typeof input !== "object" || Array.isArray(input)) throw new TypeError("data must be an object");
  const data: RightsInput = structuredClone(input); const asOf = instant(data.asOf, "asOf");
  const revision = selectRevision(data.revisions, asOf); const status = text(revision.status,"status");
  if (status === "cancelled") throw new RangeError("selected event revision is cancelled");
  if (status !== "confirmed") throw new RangeError("status must be confirmed or cancelled");
  validateTimeline(revision);
  const terms = revision.terms; if (!terms || typeof terms !== "object" || Array.isArray(terms)) throw new TypeError("terms must be an object");
  const n=positiveInteger(terms.oldShares,"oldShares"), m=positiveInteger(terms.newShares,"newShares"), denominator=n+m;
  if (denominator<=0) throw new RangeError("oldShares + newShares must be positive");
  const quoteCurrency=text(terms.quoteCurrency,"quoteCurrency"), subscriptionCurrency=text(terms.subscriptionCurrency,"subscriptionCurrency");
  const raw=positive(terms.rawCumRightsPrice,"rawCumRightsPrice"), disadvantage=finite(terms.cashDisadvantage ?? 0,"cashDisadvantage",0), reference=raw-disadvantage;
  if (reference<=0) throw new RangeError("cashDisadvantage must leave a positive referencePrice");
  const subscription=finite(terms.subscriptionPrice,"subscriptionPrice",0), fx=positive(terms.fxQuotePerSubscription,"fxQuotePerSubscription");
  if (quoteCurrency===subscriptionCurrency) { if (Math.abs(fx-1)>1e-12) throw new RangeError("same-currency events require fxQuotePerSubscription = 1"); }
  else { if (instant(terms.fxAvailableAt,"fxAvailableAt")>asOf) throw new RangeError("FX evidence was not available at asOf"); text(terms.fxSourceId,"fxSourceId"); }
  const fees=finite(terms.feesPerNewShareQuote ?? 0,"feesPerNewShareQuote",0), taxes=finite(terms.taxesPerNewShareQuote ?? 0,"taxesPerNewShareQuote",0), cash=subscription*fx+fees+taxes;
  if (cash<0) throw new RangeError("subscription cash equivalent cannot be negative");
  const inMoney=cash<reference, policy=text(terms.outOfMoneyPolicy ?? "reject","outOfMoneyPolicy");
  if (!new Set(["reject","no_adjustment"]).has(policy)) throw new RangeError("outOfMoneyPolicy must be reject or no_adjustment");
  if (!inMoney && policy==="reject") throw new RangeError("rights are not in the money under the selected reference-price convention");
  const terp=inMoney?(n*reference+m*cash)/denominator:reference, valueOld=inMoney?reference-terp:0, valueRight=inMoney?terp-cash:0;
  if (terp<=0 || valueOld<0 || valueRight<0) throw new RangeError("negative theoretical values are invalid under this reference convention");
  let entitlement: Record<string,number>|null=null;
  if (terms.holdingOldShares!==undefined) { const holding=finite(terms.holdingOldShares,"holdingOldShares",0); if(!Number.isInteger(holding))throw new RangeError("holdingOldShares must be a non-negative integer"); const exact=holding*m/n, whole=Math.floor(exact); entitlement={exactNewShares:round6(exact),wholeNewShares:whole,fractionalNewShare:round6(exact-whole)}; }
  return {eventId:revision.eventId,revisionId:revision.revisionId,availableAt:revision.availableAt,quoteCurrency,referencePrice:round6(reference),subscriptionCashQuote:round6(cash),terp:round6(terp),valuePerOldShare:round6(valueOld),valuePerNewShareRight:round6(valueRight),adjustmentFactor:round6(terp/reference),issueDiscountToTerp:inMoney?round6(1-cash/terp):null,inTheMoney:inMoney,applied:inMoney,entitlement,referencePriceIsTradedFact:false};
}
Full-height labplaygroundOpen full screen