import { describe, expect, it } from "vitest";
import { SX5E_MARKET as m, SX5E_TRF_RUN } from "../../market/sx5e";
import * as P from "..";
/**
* Review checks for the TRF formula:
* 1. consistency limit — with no funding spread and a 100% payout the hedge earns exactly
* what the TRF pays, so the fair spread must be ~0 on every tenor;
* 2. Eurex's own conversion spreads (Circular 086/21, Att. 3, appendix) exercise the daily
* funding sum Σ fwd(τ−1)·Δ_SSP(τ−1,τ) and the basis day count against a third party.
*/
describe("TRF review: consistency limit", () => {
const argsFor = (expiry: Date): P.ForwardArgs => ({
spot: m.spot,
t: P.yearFraction(m.valuationDate, expiry),
rate: m.rate,
fundingSpread: 0,
dividends: m.dividends.map((d) => ({ t: P.yearFraction(m.valuationDate, d.date), gross: d.gross })),
allIn: 1,
divScale: 1,
eqlSpread: m.eqlSpread,
trf: P.trfSchedule(m.valuationDate, expiry),
settle: P.settlementTimes(m.valuationDate, expiry),
});
it.each(SX5E_TRF_RUN.map((q) => [q.expiry.label, q.expiry.date] as const))(
"%s: zero funding spread and 100%% payout give a ~0 TRF spread",
(_, expiry) => {
// residual: pay date = ex date vs T+2 index settlement shifts dividends by ~2 days of carry
expect(Math.abs(P.fairTRFSpread(argsFor(expiry)))).toBeLessThan(0.2);
expect(Math.abs(P.fairTRFSpread({ ...argsFor(expiry), dividends: [] }))).toBeLessThan(0.01);
},
);
});
describe("TRF review: Eurex conversion spreads (18 Sep 2020)", () => {
const val = new Date("2020-09-18T00:00:00Z");
const S = 3283.69;
const fwdNodes: [string, number][] = [
["2020-09-18", 3283.69], ["2020-10-16", 3287.23], ["2020-11-20", 3283.2], ["2020-12-18", 3280.0],
["2021-01-15", 3275.77], ["2021-02-19", 3269.36], ["2021-03-19", 3266.58], ["2021-06-18", 3215.75],
["2021-09-17", 3202.0], ["2021-12-17", 3188.78], ["2022-03-18", 3177.12], ["2022-06-17", 3124.19],
["2022-09-16", 3112.92], ["2022-12-16", 3097.77], ["2023-06-16", 3032.22], ["2023-12-15", 3011.93],
["2024-12-20", 2937.22], ["2025-12-19", 2868.04], ["2026-12-18", 2800.06], ["2027-12-17", 2735.21],
["2028-12-15", 2671.48], ["2029-12-21", 2610.74],
];
const nodes = fwdNodes.map(([d, f]) => [Date.parse(`${d}T00:00:00Z`), f] as const);
// Eurex assumption 2: linear interpolation of the forward curve
const fwd = (ms: number) => {
for (let i = 1; i < nodes.length; i++) {
if (ms <= nodes[i][0]) {
const [x0, y0] = nodes[i - 1];
const [x1, y1] = nodes[i];
return y0 + ((y1 - y0) * (ms - x0)) / (x1 - x0);
}
}
return nodes[nodes.length - 1][1];
};
// [expiry, daily settlement spread Y(G+), conversion settlement spread Y(G)]
const rows: [string, number, number][] = [
["2020-12-18", -6.5, 2.0], ["2021-03-19", -0.5, 8.0], ["2021-06-18", 25.0, 33.5], ["2021-09-17", 21.0, 29.5],
["2021-12-17", 23.0, 31.5], ["2022-03-18", 26.5, 35.0], ["2022-06-17", 34.5, 43.0], ["2022-09-16", 32.5, 41.0],
["2022-12-16", 35.0, 43.5], ["2023-03-17", 41.0, 49.0], ["2023-06-16", 46.0, 54.0], ["2023-09-15", 47.0, 55.0],
["2023-12-15", 46.0, 54.0], ["2024-03-15", 50.0, 58.0], ["2024-06-21", 54.0, 62.0], ["2024-09-20", 51.0, 59.0],
["2024-12-20", 55.0, 63.0], ["2025-03-21", 60.0, 68.0], ["2025-06-20", 59.0, 67.0], ["2025-09-19", 61.0, 69.0],
["2025-12-19", 63.0, 71.0], ["2026-12-18", 70.0, 78.0], ["2027-12-17", 79.5, 87.5], ["2028-12-15", 83.5, 91.0],
["2029-12-21", 90.5, 98.0],
];
it.each(rows)("%s: %d bp → %d bp", (expiry, y, conv) => {
const sch = P.trfSchedule(val, new Date(`${expiry}T00:00:00Z`));
let sum = 0;
for (const st of sch.steps) sum += fwd(val.getTime() + st.u * P.DAYS_PER_YEAR * 86_400_000) * st.delta;
// Y(G) = Y(G+) + 8.5bp · Σ fwd(τ−1)·Δ_SSP(τ−1,τ) / (S · Δ_SSP(t,T)), rounded to 0.5bp
expect(Math.round((y + (8.5 * sum) / (S * sch.basisDelta)) * 2) / 2).toBe(conv);
});
});