src/lib/carry/curve.ts

31 lines
import type { RowState } from "../grid/rowModel";
import type { MarketData } from "../market/sx5e";
import { settlementFraction } from "../pricing";
import type { CurvePoint } from "./carry";

/**
 * A per-tenor curve for the monitor: one point per tenor (first row per expiry), at the mid of
 * the grid's bid/ask for the chosen column — so it follows edits and live repricing. Δ are the
 * settlement-adjusted ACT/360 fractions (spot settlement → expiry settlement: both the TRF
 * basis day count and the carry model's funding period), today and at the horizon date (a
 * tenor expiring on or before the horizon has Δ′ = 0).
 */
function midCurve(m: MarketData, rows: RowState[], horizon: Date, col: "trf" | "fund"): CurvePoint[] {
  const seen = new Set<string>();
  return [...rows]
    .sort((a, b) => a.expiry.date.getTime() - b.expiry.date.getTime())
    .filter((r) => !seen.has(r.expiry.label) && seen.add(r.expiry.label) && !r.error)
    .map((r) => ({
      label: r.expiry.label,
      delta: settlementFraction(m.valuationDate, r.expiry.date),
      deltaAfter: r.expiry.date.getTime() > horizon.getTime() ? settlementFraction(horizon, r.expiry.date) : 0,
      spread: (r[col].bid + r[col].ask) / 2,
    }));
}

/** TRF spreads (bp): the curve the calendar spread's legs trade on — carry, roll-down, DV01. */
export const trfCurve = (m: MarketData, rows: RowState[], horizon: Date) => midCurve(m, rows, horizon, "trf");

/** Term funding (bp) from the asset's carry model: the curve forward funding is read from. */
export const fundingCurve = (m: MarketData, rows: RowState[], horizon: Date) => midCurve(m, rows, horizon, "fund");