src/lib/carry/__tests__/simulation.test.ts

52 lines
import { describe, expect, it } from "vitest";
import { initialTable, twoWay } from "../../grid/rowModel";
import { DEFAULT_ALL_IN, SX5E_MARKET as m, SX5E_TRF_RUN } from "../../market/sx5e";
import { settlementFraction } from "../../pricing";
import { carryRoll, curveSpread, type CurvePoint } from "../carry";
import { trfCurve } from "../curve";

/**
 * Day-by-day check of carry + roll-down: walk every calendar day to the near expiry with the
 * TRF spread curve static in time-to-maturity, mark each leg's basis s_TRF(τ_d)·τ_d on today's
 * curve (residual risk hedged at no cost, so that is the whole P&L) and add up the daily P&L.
 */
describe("carry + roll-down vs a day-by-day simulation", () => {
  const rows = initialTable(m, SX5E_TRF_RUN.map((q) => ({ expiry: q.expiry, trf: twoWay(q.bid, q.offer) })), DEFAULT_ALL_IN);
  const N = 100_000_000;

  it.each([
    ["Dec26", "Dec27", false],
    ["Dec27", "Dec28", false],
    ["Dec26", "Dec28", true],
  ] as const)("%s → %s (longFar=%s)", (near, far, longFar) => {
    const nearExpiry = rows.find((r) => r.expiry.label === near)!.expiry.date;
    const curve: CurvePoint[] = trfCurve(m, rows, nearExpiry);
    const expiryOf = (label: string) => rows.find((r) => r.expiry.label === label)!.expiry.date;
    const s = longFar ? 1 : -1;
    const legs = [
      { label: near, sign: -s },
      { label: far, sign: s },
    ];
    // leg value on day d: sign · N · s_TRF(τ_d) · τ_d on today's curve (0 once expired)
    const value = (d: Date) =>
      legs.reduce((acc, leg) => {
        const T = expiryOf(leg.label);
        if (d.getTime() >= T.getTime()) return acc;
        const tau = settlementFraction(d, T);
        return acc + leg.sign * N * curveSpread(curve, tau) * 1e-4 * tau;
      }, 0);

    const daily: number[] = [];
    let prev = value(m.valuationDate);
    for (let d = new Date(m.valuationDate.getTime() + 86_400_000); d.getTime() <= nearExpiry.getTime(); d = new Date(d.getTime() + 86_400_000)) {
      const v = value(d);
      daily.push(v - prev);
      prev = v;
    }
    const simulated = daily.reduce((a, b) => a + b, 0);
    const r = carryRoll({ curve, near, far, notional: N, longFar });
    expect(simulated).toBeCloseTo(r.total, 6);

  });
});