/** Year basis for all `t` values in the engine (ACT/360): t = 1 means 360 calendar days. */
export const DAYS_PER_YEAR = 360;
const MS_PER_DAY = 86_400_000;
export const utc = (year: number, monthIndex: number, day: number) =>
new Date(Date.UTC(year, monthIndex, day));
export function yearFraction(from: Date, to: Date): number {
return (to.getTime() - from.getTime()) / MS_PER_DAY / DAYS_PER_YEAR;
}
/** Third Friday of the month: standard Eurex expiry for SX5E futures and TRFs. */
export function thirdFriday(year: number, monthIndex: number): Date {
const first = utc(year, monthIndex, 1).getUTCDay();
const firstFriday = 1 + ((5 - first + 7) % 7);
return utc(year, monthIndex, firstFriday + 14);
}
const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
export interface Expiry {
label: string; // e.g. "Dec26"
date: Date;
}
/** Parses "Dec26" → { label, date: third Friday of Dec 2026 }. */
export function expiryFromLabel(label: string): Expiry {
const m = /^([A-Za-z]{3})(\d{2})$/.exec(label.trim());
if (!m) throw new Error(`Bad expiry label: ${label}`);
const monthIndex = MONTHS.findIndex((x) => x.toLowerCase() === m[1].toLowerCase());
if (monthIndex < 0) throw new Error(`Bad month in expiry label: ${label}`);
const year = 2000 + Number(m[2]);
return { label: `${MONTHS[monthIndex]}${m[2]}`, date: thirdFriday(year, monthIndex) };
}
export const isoDate = (d: Date) => d.toISOString().slice(0, 10);
/* ---------- settlement (weekends only, no holiday calendar) ---------- */
/** Standard settlement period of the index, business days. */
export const SETTLEMENT_DAYS = 2;
/** Day count basis of the TRF. */
export const TRF_DAY_COUNT_BASIS = 360;
const isWeekend = (d: Date) => d.getUTCDay() === 0 || d.getUTCDay() === 6;
const addDays = (d: Date, n: number) => new Date(d.getTime() + n * MS_PER_DAY);
/** The day n weekdays after d (weekends skipped; holidays ignored). */
export function addWeekdays(d: Date, n: number): Date {
let out = d;
for (let k = 0; k < n; ) {
out = addDays(out, 1);
if (!isWeekend(out)) k++;
}
return out;
}
/** Calendar days between the settlement dates of t0 and t1: day(t1, n) − day(t0, n). */
export function settlementDays(t0: Date, t1: Date, n = SETTLEMENT_DAYS): number {
return Math.round((addWeekdays(t1, n).getTime() - addWeekdays(t0, n).getTime()) / MS_PER_DAY);
}
/** Δ_SSP(t0, t1): settlement-adjusted day count fraction, ACT/360. */
export const settlementFraction = (t0: Date, t1: Date, n = SETTLEMENT_DAYS) =>
settlementDays(t0, t1, n) / TRF_DAY_COUNT_BASIS;
/**
* Date-free schedule the TRF pricer needs (Eurex Circular 086/21, Att. 3):
* - basisDelta = Δ_SSP(t, T)
* - one funding step per business day τ ∈ (t, T]: the forward is read at τ−1
* (u, years from valuation) and accrues over Δ_SSP(τ−1, τ)
*/
export interface TRFSchedule {
basisDelta: number;
/** u: τ−1 in years from valuation; settle: its settlement date (τ−1 + 2 weekdays), same units. */
steps: { u: number; settle: number; delta: number }[];
}
export function trfSchedule(valuationDate: Date, expiry: Date): TRFSchedule {
const steps: TRFSchedule["steps"] = [];
let prev = valuationDate;
for (let d = addDays(valuationDate, 1); d.getTime() <= expiry.getTime(); d = addDays(d, 1)) {
if (isWeekend(d)) continue;
steps.push({
u: yearFraction(valuationDate, prev),
settle: yearFraction(valuationDate, addWeekdays(prev, SETTLEMENT_DAYS)),
delta: settlementFraction(prev, d),
});
prev = d;
}
return { basisDelta: settlementFraction(valuationDate, expiry), steps };
}
/** Spot and expiry settlement dates in years from valuation (for `ForwardArgs.settle`). */
export function settlementTimes(valuationDate: Date, expiry: Date) {
return {
spot: yearFraction(valuationDate, addWeekdays(valuationDate, SETTLEMENT_DAYS)),
expiry: yearFraction(valuationDate, addWeekdays(expiry, SETTLEMENT_DAYS)),
};
}