/**
* Tests for risk metrics
*/
import { describe, it, expect } from "vitest";
import {
computeRiskMetrics,
computeMaxDrawdown,
computeHistoricalVaR,
} from "./risk";
describe("computeRiskMetrics", () => {
it("computes basic metrics correctly", () => {
// Simple equity curve: 10% total return over 10 days
const equityCurve = [
10000, 10100, 10200, 10300, 10200, 10400, 10500, 10600, 10800, 10900, 11000,
];
const metrics = computeRiskMetrics(equityCurve);
expect(metrics.totalReturn).toBeCloseTo(0.1, 4);
expect(metrics.annualizedReturn).toBeGreaterThan(0);
expect(metrics.annualizedVol).toBeGreaterThan(0);
});
it("computes Sharpe ratio", () => {
const equityCurve = [10000, 10050, 10100, 10150, 10200, 10250];
const metrics = computeRiskMetrics(equityCurve, 0.02);
expect(metrics.sharpe).not.toBeNull();
expect(metrics.sharpe!).toBeGreaterThan(0);
});
it("returns null Sharpe when vol is zero", () => {
// Constant equity = zero vol
const equityCurve = [10000, 10000, 10000, 10000, 10000];
const metrics = computeRiskMetrics(equityCurve);
expect(metrics.annualizedVol).toBe(0);
expect(metrics.sharpe).toBeNull();
});
it("computes Sortino ratio", () => {
// Curve with some negative days
const equityCurve = [10000, 10100, 9900, 10050, 10200, 9800, 10300];
const metrics = computeRiskMetrics(equityCurve, 0.02);
expect(metrics.sortino).not.toBeNull();
});
it("computes max drawdown correctly", () => {
// Clear drawdown: peak at 11000, trough at 9000
const equityCurve = [10000, 11000, 10500, 9000, 9500, 10000];
const metrics = computeRiskMetrics(equityCurve);
// Drawdown from 11000 to 9000 = 18.18%
expect(metrics.maxDrawdown).toBeCloseTo(0.1818, 2);
});
});
describe("computeMaxDrawdown", () => {
it("returns 0 for monotonically increasing curve", () => {
const equityCurve = [100, 110, 120, 130, 140];
const dd = computeMaxDrawdown(equityCurve);
expect(dd).toBe(0);
});
it("computes correct drawdown", () => {
const equityCurve = [100, 120, 90, 110];
// Peak: 120, trough: 90, DD = 30/120 = 25%
const dd = computeMaxDrawdown(equityCurve);
expect(dd).toBeCloseTo(0.25, 4);
});
it("handles multiple drawdowns", () => {
const equityCurve = [100, 110, 100, 120, 90, 110];
// Biggest DD: 120 -> 90 = 25%
const dd = computeMaxDrawdown(equityCurve);
expect(dd).toBeCloseTo(0.25, 4);
});
});
describe("computeHistoricalVaR", () => {
it("computes 95% VaR correctly", () => {
// 100 returns from -10% to +10%
const returns: number[] = [];
for (let i = 0; i < 100; i++) {
returns.push((i - 50) / 500); // -0.1 to 0.098
}
const result = computeHistoricalVaR({ returns, confidence: 0.95 });
// 5th percentile should be around -0.09
expect(result.var).toBeGreaterThan(0.08);
expect(result.var).toBeLessThan(0.1);
});
it("computes Expected Shortfall", () => {
// More varied returns for meaningful ES calculation
const returns = [-0.10, -0.08, -0.06, -0.05, -0.04, -0.03, -0.02, -0.01, 0.01, 0.02, 0.03, 0.04, 0.05];
const result = computeHistoricalVaR({ returns, confidence: 0.9 });
expect(result.expectedShortfall).toBeDefined();
// ES should be >= VaR (average of tail losses)
expect(result.expectedShortfall!).toBeGreaterThanOrEqual(result.var);
});
it("throws for invalid confidence", () => {
expect(() =>
computeHistoricalVaR({ returns: [0.01], confidence: 1.5 })
).toThrow();
expect(() =>
computeHistoricalVaR({ returns: [0.01], confidence: 0 })
).toThrow();
});
});