/**
* Tests for SABR model
*/
import { describe, it, expect } from "vitest";
import { sabrImpliedVol, calibrateSabrSmile, buildSabrSmile } from "./sabr";
describe("sabrImpliedVol", () => {
const baseParams = {
forward: 100,
timeToMaturityYears: 1,
alpha: 0.2,
beta: 1.0,
rho: -0.3,
nu: 0.4,
};
it("returns ATM vol at strike = forward", () => {
const vol = sabrImpliedVol({ ...baseParams, strike: 100 });
expect(vol).toBeCloseTo(0.2, 1); // Should be close to alpha for beta=1
});
it("produces skew with negative rho", () => {
const otmPutVol = sabrImpliedVol({ ...baseParams, strike: 90 });
const otmCallVol = sabrImpliedVol({ ...baseParams, strike: 110 });
// With rho < 0, OTM puts should have higher IV than OTM calls
expect(otmPutVol).toBeGreaterThan(otmCallVol);
});
it("produces smile with positive nu", () => {
const atmVol = sabrImpliedVol({ ...baseParams, strike: 100 });
const otmPutVol = sabrImpliedVol({ ...baseParams, strike: 85 });
const otmCallVol = sabrImpliedVol({ ...baseParams, strike: 115 });
// Wings should be elevated relative to ATM (smile shape)
expect(otmPutVol).toBeGreaterThan(atmVol * 0.95);
expect(otmCallVol).toBeGreaterThan(atmVol * 0.85);
});
it("handles zero time to maturity", () => {
const vol = sabrImpliedVol({ ...baseParams, strike: 100, timeToMaturityYears: 0 });
expect(vol).toBe(baseParams.alpha);
});
it("returns positive vol for various strikes", () => {
const strikes = [70, 80, 90, 100, 110, 120, 130];
for (const strike of strikes) {
const vol = sabrImpliedVol({ ...baseParams, strike });
expect(vol).toBeGreaterThan(0);
expect(vol).toBeLessThan(2); // Reasonable upper bound
}
});
});
describe("buildSabrSmile", () => {
it("builds consistent smile curve", () => {
const params = {
forward: 100,
timeToMaturityYears: 0.5,
alpha: 0.25,
beta: 0.5,
rho: -0.25,
nu: 0.5,
};
const strikes = [80, 90, 100, 110, 120];
const vols = buildSabrSmile(params, strikes);
expect(vols.length).toBe(strikes.length);
vols.forEach(v => {
expect(v).toBeGreaterThan(0);
expect(v).toBeLessThan(2);
});
});
});
describe("calibrateSabrSmile", () => {
it("recovers known parameters from synthetic smile", () => {
// Generate synthetic smile from known SABR params
const trueParams = {
forward: 100,
timeToMaturityYears: 0.5,
alpha: 0.22,
beta: 1.0,
rho: -0.35,
nu: 0.45,
};
const strikes = [85, 90, 95, 100, 105, 110, 115];
const marketIvs = strikes.map(strike =>
sabrImpliedVol({ ...trueParams, strike })
);
// Calibrate
const result = calibrateSabrSmile({
forward: trueParams.forward,
timeToMaturityYears: trueParams.timeToMaturityYears,
strikes,
marketIvs,
beta: 1.0, // Fix beta
});
// Check fit quality
expect(result.error).toBeLessThan(0.01); // RMSE < 1%
// Check recovered parameters are reasonable
expect(result.alpha).toBeGreaterThan(0.1);
expect(result.alpha).toBeLessThan(0.5);
expect(result.rho).toBeGreaterThan(-1);
expect(result.rho).toBeLessThan(1);
expect(result.nu).toBeGreaterThan(0);
expect(result.nu).toBeLessThan(2);
});
it("handles flat smile", () => {
const strikes = [90, 95, 100, 105, 110];
const flatVol = 0.20;
const marketIvs = strikes.map(() => flatVol);
const result = calibrateSabrSmile({
forward: 100,
timeToMaturityYears: 1,
strikes,
marketIvs,
beta: 1.0,
});
// For flat smile, nu should be relatively small and fit should be good
expect(result.nu).toBeLessThan(0.5);
expect(result.error).toBeLessThan(0.02);
});
it("throws for insufficient data", () => {
expect(() =>
calibrateSabrSmile({
forward: 100,
timeToMaturityYears: 1,
strikes: [100, 105],
marketIvs: [0.2, 0.21],
})
).toThrow();
});
});