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);
}