src/lib/pricing/instruments.ts

127 lines
import { carriedNetDividends, divFactor, priceForward, scaledCumulativeDividends } from "./forward";
import { DAYS_PER_YEAR } from "./time";
import type { ForwardArgs } from "./types";

/** Discount factor for option premia: €STR + EQL spread. */
export function premiumDiscountFactor(a: ForwardArgs): number {
  return Math.exp(-(a.rate + a.eqlSpread) * a.t);
}

/**
 * Synthetic forward (long call / short put, European, premium upfront), quoted as
 * the stock switch — synthetic minus spot, in index points:
 *
 *   Q = C − P + K·DF − S = F·e^{−(r+EQL)t} − S
 *
 * The strike cancels by put–call parity. F is carried at r + funding spread,
 * discounted at r + EQL, so Q ≈ S·(e^{(s−EQL)t} − 1) − PV(net divs).
 */
export function fairSyntheticPrice(a: ForwardArgs): number {
  return priceForward(a) * premiumDiscountFactor(a) - a.spot;
}

/** Eurex TRF conventions. */
export const TRF_ANNUALIZATION_FACTOR = 360;
/** Share of gross dividends the TRF passes through as distributions (the hedge only receives `allIn`). */
export const TRF_DISTRIBUTION_RATIO = 1;

/** €STR as an ACT/360 overnight rate, from the continuously compounded ACT/360 rate. */
export const overnightRate360 = (rate: number) => TRF_ANNUALIZATION_FACTOR * Math.expm1(rate / DAYS_PER_YEAR);

/** (e^{Rx} − 1)/R, stable as R → 0. */
const growth = (R: number, x: number) => (Math.abs(R) < 1e-12 ? x : Math.expm1(R * x) / R);

/** Forward to date u (settling at `settle`), same convention as `priceForward`. */
function forwardAt(a: ForwardArgs, R: number, u: number, settle: number): number {
  return a.spot * Math.exp(R * (settle - (a.settle?.spot ?? 0))) - carriedNetDividends(a, u, settle);
}

/**
 * Expected funding accrued on the TRF from today to expiry, index points.
 * Accrual is simple (not compounded), as in the Eurex accrued-funding index.
 *
 * With a settlement schedule (Eurex definition, weekends-only calendar):
 *
 *   ΔAF = Σ_{τ∈(t,T]} F(τ−1) · €STR · Δ_SSP(τ−1, τ)
 *
 * Without one (no settlement lag), the daily sum is replaced by its integral
 *
 *   ΔAF ≈ €STR/360 · 360 · ∫₀ᵀ F(u) du,  in closed form (t in ACT/360 years).
 */
export function expectedAccruedFunding(a: ForwardArgs): number {
  const R = a.rate + a.fundingSpread;
  if (a.trf) {
    const l = overnightRate360(a.rate);
    let sum = 0;
    // without settlement dates on the args, read each forward unshifted (settle = u)
    for (const st of a.trf.steps) sum += forwardAt(a, R, st.u, a.settle ? st.settle : st.u) * st.delta;
    return l * sum;
  }
  // F(u) = S·e^{Ru} − allIn·k·D·e^{€STR(u − tᵢ)} once dividend i is paid
  let integral = a.spot * growth(R, a.t);
  for (const d of a.dividends) {
    if (d.t > 0 && d.t <= a.t) integral -= a.allIn * divFactor(a, d.t) * d.gross * growth(a.rate, a.t - d.t);
  }
  return (overnightRate360(a.rate) / TRF_ANNUALIZATION_FACTOR) * DAYS_PER_YEAR * integral;
}

/** Expected distributions accrued on the TRF from today to expiry (gross, not carried), index points. */
export function expectedAccruedDistributions(a: ForwardArgs): number {
  return TRF_DISTRIBUTION_RATIO * scaledCumulativeDividends(a);
}

/**
 * Fair TRF basis, index points (Eurex Circular 086/21, Attachment 3, §3.2):
 *
 *   TRF_t = S_t + Accrual_t + Basis_t,   settling at S_T + Accrual_T
 *   Basis_t = E[S_T] − S_t + E[Σ Div in (t,T]] − E[Σ S_{τ−1}·€STR·Δ(τ−1,τ)]
 *           = F − S + ΔAD − ΔAF
 *
 * E[S_T] is the hedge's forward F (futures ≈ forward: no margining/convexity adjustment).
 */
export function fairTRFBasis(a: ForwardArgs): number {
  return priceForward(a) - a.spot - expectedAccruedFunding(a) + expectedAccruedDistributions(a);
}

/**
 * TRF price in clearing notation (Eurex Circular 086/21, Attachment 3):
 *
 *   TRF_t = Accrual_t + S_t·(1 + Y·Δ_SSP(t,T))
 *
 * with Accrual_t the past distributions less funding, as published by Eurex, and
 * `basisDelta` = Δ_SSP(t,T) (see `settlementFraction`).
 */
export function trfClearingPrice(spot: number, accrual: number, spreadBps: number, basisDelta: number): number {
  return accrual + spot * (1 + spreadBps * 1e-4 * basisDelta);
}

/**
 * Fair TRF spread, bp:  Basis = I · spread · 1e-4 · Days / 360.
 * Approximately the funding spread (restated ACT/360) plus (1 − allIn)·divs pass-through.
 */
export function fairTRFSpread(a: ForwardArgs): number {
  return trfSpreadFromBasis(a, fairTRFBasis(a));
}

/** Δ_SSP(t,T) from the schedule, else calendar days / 360. */
const basisDayFraction = (a: ForwardArgs) => a.trf?.basisDelta ?? (a.t * DAYS_PER_YEAR) / TRF_ANNUALIZATION_FACTOR;

/** Basis = S · Y · Δ(t,T), Y in bp. */
export const trfBasisFromSpread = (a: ForwardArgs, spreadBps: number) => a.spot * spreadBps * 1e-4 * basisDayFraction(a);

/** Y = Basis / (S · Δ(t,T)), in bp. */
export const trfSpreadFromBasis = (a: ForwardArgs, basis: number) => (basis / (a.spot * basisDayFraction(a))) * 1e4;

/**
 * Inverse: the forward implied by a TRF spread. A future's price is its expected settlement,
 * and the TRF settles at S_T + Accrual_T with no basis, so today
 *
 *   S + Basis = F + ΔAD − ΔAF   ⇒   F = S + Basis − ΔAD + ΔAF
 *
 * ΔAF is accrued on the forward path, which depends on the funding in `a`; the grid's
 * Brent solve makes that path consistent with the quote.
 */
export function forwardFromTRF(a: ForwardArgs, spreadBps: number): number {
  return a.spot + trfBasisFromSpread(a, spreadBps) - expectedAccruedDistributions(a) + expectedAccruedFunding(a);
}