import { describe, expect, it } from "vitest";
import { initialTable, twoWay } from "../../grid/rowModel";
import { DEFAULT_ALL_IN, SX5E_MARKET, SX5E_TRF_RUN } from "../../market/sx5e";
import { settlementFraction } from "../../pricing";
import { positionGreeks } from "../greeks";
const N = 100_000_000;
const quotes = SX5E_TRF_RUN.map((q) => ({ expiry: q.expiry, trf: twoWay(q.bid, q.offer) }));
describe("calendar spread greeks", () => {
it("without dividends: delta is the basis difference, IR DV01 is second order", () => {
const m = { ...SX5E_MARKET, dividends: [] };
const rows = initialTable(m, quotes, DEFAULT_ALL_IN);
const g = positionGreeks({ market: m, rows, near: "Dec27", far: "Dec28", notional: N, longFar: false });
const d = (i: number) => settlementFraction(m.valuationDate, SX5E_TRF_RUN[i].expiry.date);
const y = (i: number) => (rows[i].trf.bid + rows[i].trf.ask) / 2;
// receive: long near (Dec27), short far (Dec28); 1% of each leg's S·(1 + YΔ)
const expected = 0.01 * N * ((1 + y(1) * 1e-4 * d(1)) - (1 + y(2) * 1e-4 * d(2)));
expect(g.delta1pct).toBeCloseTo(expected, 0);
// second order only (the hedge compounds at €STR + s, the TRF funding leg accrues at €STR):
// a few % of the ~10,111 EUR/bp forward funding DV01
expect(Math.abs(g.irDV01)).toBeLessThan(0.03 * N * 1e-4);
});
it("forward funding DV01 is bump-based: ≈ N·(Δ_far − Δ_near)·1bp × ∂TRF/∂funding", () => {
const rows = initialTable(SX5E_MARKET, quotes, DEFAULT_ALL_IN);
const rec = positionGreeks({ market: SX5E_MARKET, rows, near: "Dec26", far: "Dec27", notional: N, longFar: false });
const pay = positionGreeks({ market: SX5E_MARKET, rows, near: "Dec26", far: "Dec27", notional: N, longFar: true });
const d = (i: number) => settlementFraction(SX5E_MARKET.valuationDate, SX5E_TRF_RUN[i].expiry.date);
const analytic = N * 1e-4 * (d(1) - d(0)); // 10,111
// receiving loses when forward funding rises; the TRF moves ~1.7% more than funding at Dec27
expect(rec.forwardFundingDV01).toBeLessThan(-analytic);
expect(rec.forwardFundingDV01 / -analytic).toBeCloseTo(1.017, 2);
expect(pay.forwardFundingDV01).toBeCloseTo(-rec.forwardFundingDV01, 6);
});
it("legs are mirror images when the position is reversed", () => {
const rows = initialTable(SX5E_MARKET, quotes, DEFAULT_ALL_IN);
const rec = positionGreeks({ market: SX5E_MARKET, rows, near: "Dec26", far: "Dec27", notional: N, longFar: false });
const pay = positionGreeks({ market: SX5E_MARKET, rows, near: "Dec26", far: "Dec27", notional: N, longFar: true });
expect(pay.delta1pct).toBeCloseTo(-rec.delta1pct, 6);
expect(pay.irDV01).toBeCloseTo(-rec.irDV01, 6);
});
it("with dividends in points, the pass-through makes TRF spreads fall as spot rises", () => {
const rows = initialTable(SX5E_MARKET, quotes, DEFAULT_ALL_IN);
const g = positionGreeks({ market: SX5E_MARKET, rows, near: "Dec27", far: "Dec28", notional: N, longFar: false });
const noDivM = { ...SX5E_MARKET, dividends: [] };
const noDiv = positionGreeks({
market: noDivM,
rows: initialTable(noDivM, quotes, DEFAULT_ALL_IN),
near: "Dec27",
far: "Dec28",
notional: N,
longFar: false,
});
// short the longer tenor, whose pass-through (and its fall with spot) is larger: extra positive delta
expect(g.delta1pct).toBeGreaterThan(noDiv.delta1pct);
});
});