/**
* Historical volatility calculations
*/
import { HistoricalVolResult } from "./types";
import { logReturns, stdDev, TRADING_DAYS_PER_YEAR } from "./utils";
const DEFAULT_WINDOW = 30;
/**
* Compute historical (realized) volatility from price series
*
* Uses log returns and annualizes using √252 convention.
*
* @param prices - Array of prices (e.g., daily closes)
* @param window - Number of returns to use (default 30)
* @returns Annualized volatility and window used
*/
export function computeHistoricalVol(
prices: number[],
window?: number
): HistoricalVolResult {
if (prices.length < 2) {
throw new Error("Need at least 2 prices to compute volatility");
}
// Compute all log returns
const returns = logReturns(prices);
// Use the specified window or all available returns
const actualWindow = window
? Math.min(window, returns.length)
: Math.min(DEFAULT_WINDOW, returns.length);
if (actualWindow < 2) {
throw new Error("Need at least 2 returns to compute volatility");
}
// Take the most recent returns
const windowReturns = returns.slice(-actualWindow);
// Compute daily volatility (sample std dev)
const dailyVol = stdDev(windowReturns, true);
// Annualize
const annualizedVol = dailyVol * Math.sqrt(TRADING_DAYS_PER_YEAR);
return {
volatility: annualizedVol,
window: actualWindow,
};
}
/**
* Compute rolling historical volatility
*
* @param prices - Array of prices
* @param window - Rolling window size
* @returns Array of {timestamp index, volatility} pairs
*/
export function computeRollingVol(
prices: number[],
window: number = DEFAULT_WINDOW
): { index: number; volatility: number }[] {
const returns = logReturns(prices);
const results: { index: number; volatility: number }[] = [];
for (let i = window; i <= returns.length; i++) {
const windowReturns = returns.slice(i - window, i);
const dailyVol = stdDev(windowReturns, true);
const annualizedVol = dailyVol * Math.sqrt(TRADING_DAYS_PER_YEAR);
results.push({
index: i, // corresponds to prices[i+1] (since returns has one less element)
volatility: annualizedVol,
});
}
return results;
}
/**
* Compute Parkinson volatility (using high-low range)
* More efficient estimator than close-to-close
*
* @param highs - Array of high prices
* @param lows - Array of low prices
* @param window - Number of periods to use
* @returns Annualized Parkinson volatility
*/
export function computeParkinsonVol(
highs: number[],
lows: number[],
window?: number
): HistoricalVolResult {
if (highs.length !== lows.length) {
throw new Error("highs and lows arrays must have same length");
}
if (highs.length < 1) {
throw new Error("Need at least 1 data point");
}
const actualWindow = window
? Math.min(window, highs.length)
: Math.min(DEFAULT_WINDOW, highs.length);
// Use most recent data
const recentHighs = highs.slice(-actualWindow);
const recentLows = lows.slice(-actualWindow);
// Parkinson formula: σ = sqrt(1/(4*n*ln(2)) * Σ(ln(H/L))²)
const logRanges = recentHighs.map((h, i) => Math.log(h / recentLows[i]));
const sumSquaredLogRanges = logRanges.reduce((sum, lr) => sum + lr * lr, 0);
const factor = 1 / (4 * actualWindow * Math.LN2);
const dailyVol = Math.sqrt(factor * sumSquaredLogRanges);
const annualizedVol = dailyVol * Math.sqrt(TRADING_DAYS_PER_YEAR);
return {
volatility: annualizedVol,
window: actualWindow,
};
}