src/lib/market/sx5e.ts

93 lines
import { expiryFromLabel, thirdFriday, utc, type Expiry } from "../pricing/time";

export interface DividendPoint {
  /** Contract label, e.g. "Dec27". */
  label: string;
  /** First day of the window: the day after the previous expiry. */
  windowStart: Date;
  /** Ex-date = pay date of the bucket: the window's expiry. */
  date: Date;
  /** Gross dividend in index points going ex within the window. */
  gross: number;
}

export interface MarketData {
  valuationDate: Date;
  spot: number;
  /** €STR, continuously compounded, ACT/360. */
  rate: number;
  /** EQL spread over €STR for discounting option premia, decimal. */
  eqlSpread: number;
  dividends: DividendPoint[];
}

export interface RunQuote {
  expiry: Expiry;
  bid: number;
  offer: number;
}

/**
 * Dividend schedule from the brief, as dividend-futures-style windows (not cumulative):
 *
 *   Sep26  previous expiry (Jun26) + 1 → Sep26 expiry
 *   Dec26  Sep26 expiry + 1            → Dec26 expiry
 *   Dec27… previous Dec expiry + 1     → this Dec expiry   (yearly)
 *
 * Each window's amount goes ex — and is paid — on the window's expiry.
 */
const WINDOWS: [label: string, start: Date, end: Date, gross: number][] = [
  ["Sep26", thirdFriday(2026, 5), thirdFriday(2026, 8), 0.33],
  ["Dec26", thirdFriday(2026, 8), thirdFriday(2026, 11), 30.25],
  ["Dec27", thirdFriday(2026, 11), thirdFriday(2027, 11), 218.25],
  ["Dec28", thirdFriday(2027, 11), thirdFriday(2028, 11), 409.25],
  ["Dec29", thirdFriday(2028, 11), thirdFriday(2029, 11), 596],
  ["Dec30", thirdFriday(2029, 11), thirdFriday(2030, 11), 777],
];

const nextDay = (d: Date) => new Date(d.getTime() + 86_400_000);

export const SX5E_DIVIDENDS: DividendPoint[] = WINDOWS.map(([label, prevExpiry, expiry, gross]) => ({
  label,
  windowStart: nextDay(prevExpiry),
  date: expiry,
  gross,
}));

const VALUATION_DATE = utc(2026, 7, 11);

/**
 * TRF launch date for the accrual (simplified: the brief's valuation date, so the accrual
 * starts at 0). The valuation date can't be set before it.
 */
export const TRF_LAUNCH_DATE = VALUATION_DATE;

/** Historical €STR for the accrual since launch: flat, continuously compounded ACT/360. */
export const HISTORICAL_ESTR = 0.02;

export const DEFAULT_ALL_IN = 0.87;

export const SX5E_TRF_RUN: RunQuote[] = [
  { expiry: expiryFromLabel("Dec26"), bid: 61.5, offer: 63.5 },
  { expiry: expiryFromLabel("Dec27"), bid: 83.5, offer: 85 },
  { expiry: expiryFromLabel("Dec28"), bid: 89, offer: 91.5 },
  { expiry: expiryFromLabel("Dec29"), bid: 94, offer: 96 },
  { expiry: expiryFromLabel("Dec30"), bid: 97, offer: 101 },
];

export const EXPIRIES: Expiry[] = SX5E_TRF_RUN.map((q) => q.expiry);

/**
 * "SX5E TRF vs 19.255 basis" — meaning still open. It is not the Dec26 TRF basis at the
 * real spot (that is 14.90 at the 62.5bp mid, with settlement lags).
 */
export const TRF_REFERENCE_BASIS = 19.255;

export const SX5E_MARKET: MarketData = {
  valuationDate: VALUATION_DATE,
  spot: 6551.22, // SX5E close, 11-Aug-2026
  rate: 0.02, // PLACEHOLDER — flat; replace with the €STR curve
  eqlSpread: 0.005, // flat
  dividends: SX5E_DIVIDENDS,
};