/**
* Spot delta and IR DV01 of the TRF calendar spread.
*
* Each TRF leg is marked at its basis on top of the index, S · (1 + Y · Δ) index points per
* contract (the accrual to date does not move), so for notional N = contracts · multiplier · S₀:
*
* P&L_leg = sign · N / S₀ · [ S₁ · (1 + Y₁ · Δ) − S₀ · (1 + Y₀ · Δ) ]
*
* Holding the quoted spreads fixed, delta would just be the basis difference between the legs
* and IR DV01 would be exactly 0 (€STR does not enter the TRF price). So the market is bumped
* with the *assumptions* held — term funding, dividends, all-in — and the TRF spreads re-solved
* (every row on "solve for prices"): that picks up the dividend pass-through moving with spot
* (dividends are fixed in points) and the €STR carry of the hedge.
*/
import { editTable, setSolveForTable, type RowState } from "../grid/rowModel";
import type { MarketData } from "../market/sx5e";
import { settlementFraction } from "../pricing";
export interface LegGreeks {
label: string;
sign: number;
/** P&L for +1% spot, EUR. */
delta1pct: number;
/** P&L for +1bp €STR, EUR. */
irDV01: number;
}
export interface PositionGreeks {
legs: [LegGreeks, LegGreeks];
delta1pct: number;
irDV01: number;
/**
* P&L for +1bp of Forward Funding between the two tenors (carry-model terms): the far tenor's
* term funding is raised by (τ_far − τ_near)/τ_far bp with the near tenor and every other
* assumption held, and the TRF spreads re-solved.
*/
forwardFundingDV01: number;
}
const SPOT_BUMP = 0.01; // +1%
const RATE_BUMP = 1e-4; // +1bp
/** TRF mid spread (bp) per expiry label, for rows already priced from their assumptions. */
function midsOf(priced: RowState[]): Map<string, number> {
const out = new Map<string, number>();
for (const r of priced) if (!out.has(r.expiry.label)) out.set(r.expiry.label, (r.trf.bid + r.trf.ask) / 2);
return out;
}
/** TRF mid spread (bp) per expiry label, with every row priced from its assumptions. */
const trfMids = (m: MarketData, rows: RowState[]) => midsOf(setSolveForTable(m, rows, "all", "price"));
export function positionGreeks(p: {
market: MarketData;
rows: RowState[];
near: string;
far: string;
notional: number;
longFar: boolean;
}): PositionGreeks {
const { market: m, rows, notional: N } = p;
const priced = setSolveForTable(m, rows, "all", "price");
const base = midsOf(priced);
const spotUp = { ...m, spot: m.spot * (1 + SPOT_BUMP) };
const rateUp = { ...m, rate: m.rate + RATE_BUMP };
const atSpot = trfMids(spotUp, rows);
const atRate = trfMids(rateUp, rows);
const leg = (label: string, sign: number): LegGreeks => {
const row = rows.find((r) => r.expiry.label === label);
if (!row) throw new Error("Unknown tenor");
const delta = settlementFraction(m.valuationDate, row.expiry.date);
const value = (s: number, y: number) => s * (1 + y * 1e-4 * delta);
const y0 = base.get(label)!;
const pnl = (s1: number, y1: number) => (sign * N * (value(s1, y1) - value(m.spot, y0))) / m.spot;
return {
label,
sign,
delta1pct: pnl(spotUp.spot, atSpot.get(label)!),
irDV01: pnl(m.spot, atRate.get(label)!),
};
};
const s = p.longFar ? 1 : -1;
const legs: [LegGreeks, LegGreeks] = [leg(p.near, -s), leg(p.far, s)];
// Forward Funding +1bp: far term funding += (τ_far − τ_near)/τ_far bp, near and the rest held
const fi = priced.findIndex((r) => r.expiry.label === p.far);
const ni = priced.findIndex((r) => r.expiry.label === p.near);
const tau = (i: number) => settlementFraction(m.valuationDate, priced[i].expiry.date);
const bump = (tau(fi) - tau(ni)) / tau(fi);
let bumped = editTable(m, priced, fi, "fund", "bid", priced[fi].fund.bid + bump);
bumped = editTable(m, bumped, fi, "fund", "ask", priced[fi].fund.ask + bump);
const y1 = midsOf(bumped);
const spreadPnl = (label: string, i: number, sign: number) =>
sign * N * ((y1.get(label)! - base.get(label)!) * 1e-4) * tau(i);
return {
legs,
delta1pct: legs[0].delta1pct + legs[1].delta1pct,
irDV01: legs[0].irDV01 + legs[1].irDV01,
forwardFundingDV01: spreadPnl(p.near, ni, -s) + spreadPnl(p.far, fi, s),
};
}