import { describe, expect, it } from "vitest";
import {
brent,
carriedGrossDividends,
DAYS_PER_YEAR,
expectedAccruedFunding,
expiryFromLabel,
fairSyntheticPrice,
fairTRFBasis,
fairTRFSpread,
forwardFromTRF,
overnightRate360,
addWeekdays,
settlementDays,
settlementFraction,
trfSchedule,
trfClearingPrice,
implied,
premiumDiscountFactor,
priceForward,
thirdFriday,
utc,
yearFraction,
type ForwardArgs,
} from "..";
const base: ForwardArgs = {
spot: 5400,
t: 2.35,
rate: 0.021,
fundingSpread: 0.0062,
dividends: [
{ t: 0.1, gross: 0.33 },
{ t: 0.35, gross: 29.92 },
{ t: 1.35, gross: 188 },
{ t: 2.35, gross: 191 },
{ t: 3.35, gross: 186.75 }, // after expiry: must be ignored
],
allIn: 0.87,
divScale: 1,
eqlSpread: 0.005,
};
describe("time", () => {
it("uses an ACT/360 year", () => {
expect(DAYS_PER_YEAR).toBe(360);
expect(yearFraction(utc(2026, 0, 1), utc(2027, 0, 1))).toBeCloseTo(365 / 360, 15);
});
it("finds third Fridays", () => {
expect(thirdFriday(2026, 11).toISOString().slice(0, 10)).toBe("2026-12-18");
expect(expiryFromLabel("Dec27").date.toISOString().slice(0, 10)).toBe("2027-12-17");
});
});
describe("priceForward", () => {
it("matches S·e^{Rt} with no dividends", () => {
const a = { ...base, dividends: [] };
expect(priceForward(a)).toBeCloseTo(5400 * Math.exp(0.0272 * 2.35), 10);
});
it("with settlement: S carried settle-to-settle at R, divs carried pay-to-settle at €STR", () => {
const settle = { spot: 2 / DAYS_PER_YEAR, expiry: base.t + 4 / DAYS_PER_YEAR };
const R = base.rate + base.fundingSpread;
const fv = 0.87 * [0.33, 29.92, 188, 191].reduce(
(acc, d, i) => acc + d * Math.exp(base.rate * (settle.expiry - [0.1, 0.35, 1.35, 2.35][i])),
0,
);
const manual = 5400 * Math.exp(R * (settle.expiry - settle.spot)) - fv;
expect(priceForward({ ...base, settle })).toBeCloseTo(manual, 10);
});
it("ignores dividends after expiry", () => {
const r = base.rate;
const manual =
0.33 * Math.exp(r * 2.25) + 29.92 * Math.exp(r * 2.0) + 188 * Math.exp(r * 1.0) + 191;
expect(carriedGrossDividends(base)).toBeCloseTo(manual, 10);
});
});
describe("synthetic (switch vs spot)", () => {
it("is F·e^{−(r+EQL)t} − S", () => {
const df = Math.exp(-(0.021 + 0.005) * 2.35);
expect(fairSyntheticPrice(base)).toBeCloseTo(priceForward(base) * df - 5400, 10);
});
it("is zero with no dividends when funding = EQL", () => {
const a = { ...base, dividends: [], fundingSpread: base.eqlSpread };
expect(fairSyntheticPrice(a)).toBeCloseTo(0, 10);
});
it("is nearly spot-insensitive when funding ≈ EQL: ∂Q/∂S = e^{(s−EQL)t} − 1", () => {
const bump = fairSyntheticPrice({ ...base, spot: 5401 }) - fairSyntheticPrice(base);
expect(bump).toBeCloseTo(Math.exp((0.0062 - 0.005) * 2.35) - 1, 10);
});
});
describe("TRF", () => {
it("accrued funding: closed-form integral matches a daily sum of forwards", () => {
const days = Math.round(base.t * DAYS_PER_YEAR);
const a = { ...base, t: days / DAYS_PER_YEAR };
let sum = 0;
for (let d = 0; d < days; d++) sum += priceForward({ ...a, t: d / DAYS_PER_YEAR });
const daily = (overnightRate360(a.rate) / 360) * sum;
// continuous integral vs left Riemann sum: agree to well under 0.1 index point
expect(Math.abs(expectedAccruedFunding(a) - daily)).toBeLessThan(0.1);
});
it("inverse: forwardFromTRF at the fair spread recovers the forward", () => {
expect(forwardFromTRF(base, fairTRFSpread(base))).toBeCloseTo(priceForward(base), 8);
});
it("is ~0 with no dividends and no funding spread", () => {
expect(fairTRFSpread({ ...base, dividends: [], fundingSpread: 0 })).toBeCloseTo(0, 1);
});
it("no dividends: funding spread restated ACT/360, compounded on the growing index", () => {
// Basis = S·(e^{Rt} − 1)·s/R, quoted simple on today's index: TRF = s·(360/Y)·(e^{Rt} − 1)/(Rt)
const R = base.rate + 0.01;
const trf = fairTRFSpread({ ...base, dividends: [], fundingSpread: 0.01 });
expect(trf).toBeCloseTo(100 * (360 / DAYS_PER_YEAR) * (Math.expm1(R * base.t) / (R * base.t)), 1);
});
it("passes (1 − allIn) of dividends through to the basis", () => {
const d = (allIn: number) => fairTRFBasis({ ...base, allIn, divScale: 1.01 }) - fairTRFBasis({ ...base, allIn });
const gross = 0.01 * (0.33 + 29.92 + 188 + 191);
expect(d(0.87)).toBeGreaterThan(0.1 * gross); // ≈ 13% of the bump, less carry
expect(Math.abs(d(1))).toBeLessThan(0.05 * gross); // full pass-through: only carry remains
});
});
describe("settlement (2 weekdays, no holidays)", () => {
const iso = (d: Date) => d.toISOString().slice(0, 10);
it("skips weekends", () => {
expect(iso(addWeekdays(utc(2026, 7, 11), 2))).toBe("2026-08-13"); // Tue → Thu
expect(iso(addWeekdays(utc(2026, 11, 18), 2))).toBe("2026-12-22"); // Fri → Tue
});
it("Dec26 basis runs Thu 13 Aug → Tue 22 Dec: 131 days", () => {
expect(settlementDays(utc(2026, 7, 11), utc(2026, 11, 18))).toBe(131);
});
it("schedule: one step per business day, deltas sum to the basis day count", () => {
const s = trfSchedule(utc(2026, 7, 11), utc(2026, 11, 18));
expect(s.steps).toHaveLength(93); // weekdays in (11 Aug, 18 Dec]: 18 weeks + Wed–Fri
expect(s.steps.reduce((a, x) => a + x.delta, 0)).toBeCloseTo(s.basisDelta, 12);
// weekends accrue on the step whose settlement window spans them (Wed → Thu settles Fri → Mon)
expect(Math.max(...s.steps.map((x) => x.delta * 360))).toBe(3);
});
it("discrete funding sum ≈ the continuous integral", () => {
const val = utc(2026, 7, 11);
const exp = utc(2027, 11, 17);
const a = { ...base, t: yearFraction(val, exp), dividends: [] };
const discrete = expectedAccruedFunding({ ...a, trf: trfSchedule(val, exp) });
// the schedule is shifted by settlement (+0 to +2 days over the whole period): within ~0.5%
expect(discrete / expectedAccruedFunding(a)).toBeCloseTo(1, 2);
});
});
describe("TRF clearing notation vs Eurex Circular 086/21, Attachment 3, appendix (18 Sep 2020)", () => {
// TRF_t = Accrual + S·(1 + Y·Δ_SSP), weekends-only settlement calendar
const S = 3283.69;
const accrual = 490.96;
const val = utc(2020, 8, 18);
const rows: [string, number, number][] = [
["2020-12-18", -6.5, 3774.11], ["2021-03-19", -0.5, 3774.57], ["2021-06-18", 25.0, 3780.88],
["2021-09-17", 21.0, 3781.63], ["2021-12-17", 23.0, 3784.2], ["2022-03-18", 26.5, 3787.85],
["2022-06-17", 34.5, 3794.7], ["2022-09-16", 32.5, 3796.23], ["2022-12-16", 35.0, 3800.8],
["2023-03-17", 41.0, 3808.68], ["2023-06-16", 46.0, 3816.65], ["2023-09-15", 47.0, 3821.47],
["2023-12-15", 46.0, 3824.29], ["2024-03-15", 50.0, 3832.76], ["2024-06-21", 54.0, 3842.23],
["2024-09-20", 51.0, 3842.71], ["2024-12-20", 55.0, 3852.61], ["2025-03-21", 60.0, 3864.68],
["2025-06-20", 59.0, 3868.08], ["2025-09-19", 61.0, 3876.31], ["2025-12-19", 63.0, 3884.87],
["2026-12-18", 70.0, 3920.36], ["2027-12-17", 79.5, 3966.53], ["2028-12-15", 83.5, 4003.9],
];
const delta = (iso: string) => settlementFraction(val, new Date(iso + "T00:00:00Z"));
it.each(rows)("%s: spread %d bp → %d", (iso, spread, price) => {
expect(Math.abs(trfClearingPrice(S, accrual, spread, delta(iso)) - price)).toBeLessThan(0.01);
});
it("DEC29 is off by exactly the 2 Christmas holidays (no TARGET2 calendar)", () => {
const diff = 4053.91 - trfClearingPrice(S, accrual, 90.5, delta("2029-12-21"));
expect(diff).toBeCloseTo(S * 90.5e-4 * (2 / 360), 2);
});
});
describe("brent", () => {
it("solves a cubic", () => {
expect(brent((x) => x ** 3 - 2 * x - 5, 2, 3)).toBeCloseTo(2.0945514815423265, 12);
});
});
describe("implied — Brent vs closed form (synthetic quote)", () => {
const Q = fairSyntheticPrice(base);
const df = premiumDiscountFactor(base);
const R = base.rate + base.fundingSpread;
const A = base.spot * Math.exp(R * base.t);
const D = carriedGrossDividends(base);
it("funding spread, no dividends: s = ln((Q+S)/(DF·S))/t − r", () => {
const a = { ...base, dividends: [] };
const Qa = fairSyntheticPrice(a);
const closed = Math.log((Qa + a.spot) / (df * a.spot)) / a.t - a.rate;
expect(implied(fairSyntheticPrice, "fundingSpread", Qa, { ...a, fundingSpread: 0 })).toBeCloseTo(closed, 12);
});
it("all-in is linear: allIn = (A − (Q+S)/DF) / (k·ΣD̃)", () => {
const target = Q - 10;
const closed = (A - (target + base.spot) / df) / (base.divScale * D);
expect(implied(fairSyntheticPrice, "allIn", target, base)).toBeCloseTo(closed, 12);
});
it("dividend scale is linear", () => {
const target = Q + 15;
const closed = (A - (target + base.spot) / df) / (base.allIn * D);
expect(implied(fairSyntheticPrice, "divScale", target, base)).toBeCloseTo(closed, 12);
});
it("spot is linear but ill-conditioned: S = (Q + DF·allIn·k·ΣD̃) / (DF·e^{Rt} − 1)", () => {
const target = Q + 0.5;
const closed = (target + df * base.allIn * base.divScale * D) / (df * Math.exp(R * base.t) - 1);
expect(implied(fairSyntheticPrice, "spot", target, base)).toBeCloseTo(closed, 6);
});
it("round-trips every solvable key through the synthetic", () => {
for (const key of ["rate", "fundingSpread", "allIn", "divScale", "eqlSpread"] as const) {
const x = implied(fairSyntheticPrice, key, Q, { ...base, [key]: base[key] * 1.3 + 0.001 });
expect(x).toBeCloseTo(base[key], 10);
}
});
it("round-trips funding through the TRF", () => {
const quote = fairTRFSpread(base);
expect(implied(fairTRFSpread, "fundingSpread", quote, { ...base, fundingSpread: 0 })).toBeCloseTo(0.0062, 12);
});
it("widens the bracket when the root is outside", () => {
const quote = fairTRFSpread({ ...base, fundingSpread: 0.35 });
expect(implied(fairTRFSpread, "fundingSpread", quote, base)).toBeCloseTo(0.35, 10);
});
});