/**
* Tests for Heston model
*/
import { describe, it, expect } from "vitest";
import {
simulateHestonPaths,
priceOptionHestonMonteCarlo,
hestonDistributionStats,
} from "./heston";
describe("simulateHestonPaths", () => {
const baseParams = {
spot: 100,
rate: 0.05,
kappa: 2.0,
theta: 0.04, // Long-term variance (20% vol)
xi: 0.3,
rho: -0.7,
v0: 0.04, // Initial variance (20% vol)
timeToMaturityYears: 1,
steps: 50,
paths: 10000,
};
it("produces positive terminal prices", () => {
const result = simulateHestonPaths(baseParams);
expect(result.terminalPrices.length).toBe(baseParams.paths);
result.terminalPrices.forEach(price => {
expect(price).toBeGreaterThan(0);
});
});
it("produces positive terminal variances", () => {
const result = simulateHestonPaths(baseParams);
result.terminalVariances.forEach(v => {
expect(v).toBeGreaterThanOrEqual(0);
});
});
it("has mean near forward price", () => {
const result = simulateHestonPaths({ ...baseParams, paths: 50000 });
const meanPrice = result.terminalPrices.reduce((a, b) => a + b, 0) / result.terminalPrices.length;
// Expected forward ≈ spot * exp(r * T)
const expectedForward = baseParams.spot * Math.exp(baseParams.rate * baseParams.timeToMaturityYears);
// Allow 5% error due to Monte Carlo variance
expect(meanPrice).toBeGreaterThan(expectedForward * 0.95);
expect(meanPrice).toBeLessThan(expectedForward * 1.05);
});
it("throws for too many paths", () => {
expect(() =>
simulateHestonPaths({ ...baseParams, paths: 300000 })
).toThrow();
});
it("produces higher vol with higher xi", () => {
const result1 = simulateHestonPaths({ ...baseParams, xi: 0.2, paths: 20000 });
const result2 = simulateHestonPaths({ ...baseParams, xi: 0.6, paths: 20000 });
const std1 = standardDeviation(result1.terminalPrices);
const std2 = standardDeviation(result2.terminalPrices);
// Higher vol of vol should produce higher price dispersion
expect(std2).toBeGreaterThan(std1 * 0.8);
});
});
describe("priceOptionHestonMonteCarlo", () => {
const baseParams = {
spot: 100,
rate: 0.05,
kappa: 2.0,
theta: 0.04,
xi: 0.3,
rho: -0.7,
v0: 0.04,
timeToMaturityYears: 0.5,
steps: 30,
paths: 30000,
};
it("prices ATM call reasonably", () => {
const result = priceOptionHestonMonteCarlo({
...baseParams,
strike: 100,
optionType: "call",
});
// ATM call should have positive value
expect(result.price).toBeGreaterThan(0);
// Should be in reasonable range (roughly BS-like)
expect(result.price).toBeLessThan(20);
// Standard error should be small relative to price
expect(result.standardError).toBeLessThan(result.price * 0.1);
});
it("prices OTM put cheaper than ATM", () => {
const atmResult = priceOptionHestonMonteCarlo({
...baseParams,
strike: 100,
optionType: "put",
});
const otmResult = priceOptionHestonMonteCarlo({
...baseParams,
strike: 85,
optionType: "put",
});
expect(otmResult.price).toBeLessThan(atmResult.price);
});
it("produces tighter CI with more paths", () => {
const result1 = priceOptionHestonMonteCarlo({
...baseParams,
strike: 100,
optionType: "call",
paths: 10000,
});
const result2 = priceOptionHestonMonteCarlo({
...baseParams,
strike: 100,
optionType: "call",
paths: 50000,
});
const ciWidth1 = result1.confidenceInterval.upper - result1.confidenceInterval.lower;
const ciWidth2 = result2.confidenceInterval.upper - result2.confidenceInterval.lower;
// More paths should give tighter CI
expect(ciWidth2).toBeLessThan(ciWidth1);
});
});
describe("hestonDistributionStats", () => {
it("computes distribution statistics", () => {
const params = {
spot: 100,
rate: 0.05,
kappa: 2.0,
theta: 0.04,
xi: 0.3,
rho: -0.7,
v0: 0.04,
timeToMaturityYears: 0.5,
steps: 30,
paths: 20000,
};
const stats = hestonDistributionStats(params);
// Basic sanity checks
expect(stats.mean).toBeGreaterThan(0);
expect(stats.median).toBeGreaterThan(0);
expect(stats.std).toBeGreaterThan(0);
// Percentiles should be ordered
expect(stats.percentile5).toBeLessThan(stats.percentile25);
expect(stats.percentile25).toBeLessThan(stats.median);
expect(stats.median).toBeLessThan(stats.percentile75);
expect(stats.percentile75).toBeLessThan(stats.percentile95);
});
});
// Helper function
function standardDeviation(arr: number[]): number {
const n = arr.length;
const mean = arr.reduce((a, b) => a + b, 0) / n;
const variance = arr.reduce((sum, val) => sum + (val - mean) ** 2, 0) / (n - 1);
return Math.sqrt(variance);
}