add volitility-models

This commit is contained in:
Bojan Kucera 2025-06-03 15:03:00 -04:00
parent dda9f23285
commit 4397541d2c
4 changed files with 461 additions and 68 deletions

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,460 @@
/**
* Volatility Models
* Advanced volatility modeling and forecasting tools
*/
// Local interface definition to avoid circular dependency
interface OHLCVData {
open: number;
high: number;
low: number;
close: number;
volume: number;
timestamp: Date;
}
export interface GARCHParameters {
omega: number; // Constant term
alpha: number; // ARCH parameter
beta: number; // GARCH parameter
logLikelihood: number;
aic: number;
bic: number;
}
export interface VolatilityEstimates {
closeToClose: number;
parkinson: number;
garmanKlass: number;
rogersSatchell: number;
yangZhang: number;
}
export interface VolatilityRegime {
regime: number;
startDate: Date;
endDate: Date;
averageVolatility: number;
observations: number;
}
export interface VolatilityTerm {
maturity: number; // Days to maturity
impliedVolatility: number;
confidence: number;
}
export interface HestonParameters {
kappa: number; // Mean reversion speed
theta: number; // Long-term variance
sigma: number; // Volatility of variance
rho: number; // Correlation
v0: number; // Initial variance
logLikelihood: number;
}
/**
* Calculate realized volatility using different estimators
*/
export function calculateRealizedVolatility(
ohlcv: OHLCVData[],
annualizationFactor: number = 252
): VolatilityEstimates {
if (ohlcv.length < 2) {
throw new Error('Need at least 2 observations for volatility calculation');
}
const n = ohlcv.length;
let closeToCloseSum = 0;
let parkinsonSum = 0;
let garmanKlassSum = 0;
let rogersSatchellSum = 0;
let yangZhangSum = 0;
// Calculate log returns and volatility estimators
for (let i = 1; i < n; i++) {
const prev = ohlcv[i - 1];
const curr = ohlcv[i];
// Close-to-close
const logReturn = Math.log(curr.close / prev.close);
closeToCloseSum += logReturn * logReturn;
// Parkinson estimator
const logHighLow = Math.log(curr.high / curr.low);
parkinsonSum += logHighLow * logHighLow;
// Garman-Klass estimator
const logOpenClose = Math.log(curr.close / curr.open);
garmanKlassSum += 0.5 * logHighLow * logHighLow - (2 * Math.log(2) - 1) * logOpenClose * logOpenClose;
// Rogers-Satchell estimator
const logHighOpen = Math.log(curr.high / curr.open);
const logHighClose = Math.log(curr.high / curr.close);
const logLowOpen = Math.log(curr.low / curr.open);
const logLowClose = Math.log(curr.low / curr.close);
rogersSatchellSum += logHighOpen * logHighClose + logLowOpen * logLowClose;
// Yang-Zhang estimator components
const overnight = Math.log(curr.open / prev.close);
yangZhangSum += overnight * overnight + rogersSatchellSum / i; // Simplified for brevity
}
return {
closeToClose: Math.sqrt((closeToCloseSum / (n - 1)) * annualizationFactor),
parkinson: Math.sqrt((parkinsonSum / (n - 1) / (4 * Math.log(2))) * annualizationFactor),
garmanKlass: Math.sqrt((garmanKlassSum / (n - 1)) * annualizationFactor),
rogersSatchell: Math.sqrt((rogersSatchellSum / (n - 1)) * annualizationFactor),
yangZhang: Math.sqrt((yangZhangSum / (n - 1)) * annualizationFactor)
};
}
/**
* Estimate GARCH(1,1) model parameters
*/
export function estimateGARCH(
returns: number[],
maxIterations: number = 100,
tolerance: number = 1e-6
): GARCHParameters {
const n = returns.length;
// Initial parameter estimates
let omega = 0.01;
let alpha = 0.05;
let beta = 0.9;
// Calculate unconditional variance
const meanReturn = returns.reduce((sum, r) => sum + r, 0) / n;
const unconditionalVar = returns.reduce((sum, r) => sum + Math.pow(r - meanReturn, 2), 0) / (n - 1);
let logLikelihood = -Infinity;
for (let iter = 0; iter < maxIterations; iter++) {
const variances: number[] = [unconditionalVar];
let newLogLikelihood = 0;
// Calculate conditional variances
for (let t = 1; t < n; t++) {
const prevVar = variances[t - 1];
const prevReturn = returns[t - 1] - meanReturn;
const currentVar = omega + alpha * prevReturn * prevReturn + beta * prevVar;
variances.push(Math.max(currentVar, 1e-8)); // Ensure positive variance
// Add to log-likelihood
const currentReturn = returns[t] - meanReturn;
newLogLikelihood -= 0.5 * (Math.log(2 * Math.PI) + Math.log(currentVar) +
(currentReturn * currentReturn) / currentVar);
}
// Check for convergence
if (Math.abs(newLogLikelihood - logLikelihood) < tolerance) {
break;
}
logLikelihood = newLogLikelihood;
// Simple gradient update (in practice, use more sophisticated optimization)
const gradientStep = 0.001;
omega = Math.max(0.001, omega + gradientStep);
alpha = Math.max(0.001, Math.min(0.999, alpha + gradientStep));
beta = Math.max(0.001, Math.min(0.999 - alpha, beta + gradientStep));
}
// Calculate information criteria
const k = 3; // Number of parameters
const aic = -2 * logLikelihood + 2 * k;
const bic = -2 * logLikelihood + k * Math.log(n);
return {
omega,
alpha,
beta,
logLikelihood,
aic,
bic
};
}
/**
* Calculate EWMA volatility
*/
export function calculateEWMAVolatility(
returns: number[],
lambda: number = 0.94,
annualizationFactor: number = 252
): number[] {
const n = returns.length;
const volatilities: number[] = [];
// Initialize with sample variance
const meanReturn = returns.reduce((sum, r) => sum + r, 0) / n;
let variance = returns.reduce((sum, r) => sum + Math.pow(r - meanReturn, 2), 0) / (n - 1);
for (let t = 0; t < n; t++) {
if (t > 0) {
const prevReturn = returns[t - 1] - meanReturn;
variance = lambda * variance + (1 - lambda) * prevReturn * prevReturn;
}
volatilities.push(Math.sqrt(variance * annualizationFactor));
}
return volatilities;
}
/**
* Identify volatility regimes
*/
export function identifyVolatilityRegimes(
returns: number[],
numRegimes: number = 3,
windowSize: number = 60
): VolatilityRegime[] {
// Calculate rolling volatility
const rollingVol: number[] = [];
const timestamps: Date[] = [];
for (let i = windowSize - 1; i < returns.length; i++) {
const window = returns.slice(i - windowSize + 1, i + 1);
const mean = window.reduce((sum, r) => sum + r, 0) / window.length;
const variance = window.reduce((sum, r) => sum + Math.pow(r - mean, 2), 0) / (window.length - 1);
rollingVol.push(Math.sqrt(variance * 252)); // Annualized
timestamps.push(new Date(Date.now() + i * 24 * 60 * 60 * 1000)); // Mock timestamps
}
// Simple k-means clustering on absolute returns
const absReturns = returns.map(ret => Math.abs(ret));
const sortedReturns = [...absReturns].sort((a, b) => a - b);
// Define regime thresholds
const thresholds: number[] = [];
for (let i = 1; i < numRegimes; i++) {
const index = Math.floor((i / numRegimes) * sortedReturns.length);
thresholds.push(sortedReturns[index]);
}
// Classify returns into regimes
const regimeSequence = absReturns.map(absRet => {
for (let i = 0; i < thresholds.length; i++) {
if (absRet <= thresholds[i]) return i;
}
return numRegimes - 1;
});
// Calculate regime statistics
const regimes: VolatilityRegime[] = [];
for (let regime = 0; regime < numRegimes; regime++) {
const regimeIndices = regimeSequence
.map((r, idx) => r === regime ? idx : -1)
.filter(idx => idx !== -1);
if (regimeIndices.length > 0) {
const regimeVolatilities = regimeIndices.map(idx =>
idx < rollingVol.length ? rollingVol[idx] : 0
);
const avgVol = regimeVolatilities.reduce((sum, vol) => sum + vol, 0) / regimeVolatilities.length;
regimes.push({
regime,
startDate: new Date(Date.now()),
endDate: new Date(Date.now() + regimeIndices.length * 24 * 60 * 60 * 1000),
averageVolatility: avgVol,
observations: regimeIndices.length
});
}
}
return regimes;
}
/**
* Calculate volatility term structure
*/
export function calculateVolatilityTermStructure(
spotVol: number,
maturities: number[],
meanReversion: number = 0.5
): VolatilityTerm[] {
return maturities.map(maturity => {
// Simple mean reversion model for term structure
const timeToMaturity = maturity / 365; // Convert to years
const termVolatility = spotVol * Math.exp(-meanReversion * timeToMaturity);
return {
maturity,
impliedVolatility: Math.max(termVolatility, 0.01), // Floor at 1%
confidence: Math.exp(-timeToMaturity) // Confidence decreases with maturity
};
});
}
/**
* Calculate volatility smile/skew parameters
*/
export function calculateVolatilitySmile(
strikes: number[],
spotPrice: number,
impliedVols: number[]
): {
atmVolatility: number;
skew: number;
convexity: number;
riskReversal: number;
} {
if (strikes.length !== impliedVols.length || strikes.length < 3) {
throw new Error('Need at least 3 strikes with corresponding implied volatilities');
}
// Find ATM volatility
const atmIndex = strikes.reduce((closest, strike, idx) =>
Math.abs(strike - spotPrice) < Math.abs(strikes[closest] - spotPrice) ? idx : closest, 0
);
const atmVolatility = impliedVols[atmIndex];
// Calculate skew (derivative at ATM)
let skew = 0;
if (atmIndex > 0 && atmIndex < strikes.length - 1) {
const deltaStrike = strikes[atmIndex + 1] - strikes[atmIndex - 1];
const deltaVol = impliedVols[atmIndex + 1] - impliedVols[atmIndex - 1];
skew = deltaVol / deltaStrike;
}
// Calculate convexity (second derivative)
let convexity = 0;
if (atmIndex > 0 && atmIndex < strikes.length - 1) {
const h = strikes[atmIndex + 1] - strikes[atmIndex];
convexity = (impliedVols[atmIndex + 1] - 2 * impliedVols[atmIndex] + impliedVols[atmIndex - 1]) / (h * h);
}
// Risk reversal (put-call vol difference)
const otmPutIndex = strikes.findIndex(strike => strike < spotPrice * 0.9);
const otmCallIndex = strikes.findIndex(strike => strike > spotPrice * 1.1);
let riskReversal = 0;
if (otmPutIndex !== -1 && otmCallIndex !== -1) {
riskReversal = impliedVols[otmCallIndex] - impliedVols[otmPutIndex];
}
return {
atmVolatility,
skew,
convexity,
riskReversal
};
}
/**
* Estimate Heston stochastic volatility model parameters
*/
export function estimateHestonParameters(
returns: number[],
maxIterations: number = 100
): HestonParameters {
const n = returns.length;
// Initial parameter estimates
let kappa = 2.0; // Mean reversion speed
let theta = 0.04; // Long-term variance
let sigma = 0.3; // Vol of vol
let rho = -0.5; // Correlation
let v0 = 0.04; // Initial variance
let logLikelihood = -Infinity;
for (let iter = 0; iter < maxIterations; iter++) {
const variances: number[] = [v0];
let newLogLikelihood = 0;
// Euler discretization of Heston model
const dt = 1 / 252; // Daily time step
for (let t = 1; t < n; t++) {
const prevVar = Math.max(variances[t - 1], 1e-8);
const sqrtVar = Math.sqrt(prevVar);
// Simulate variance process (simplified)
const dW2 = Math.random() - 0.5; // Should be proper random normal
const newVar = prevVar + kappa * (theta - prevVar) * dt + sigma * sqrtVar * Math.sqrt(dt) * dW2;
variances.push(Math.max(newVar, 1e-8));
// Calculate likelihood contribution
const expectedReturn = 0; // Assuming zero drift for simplicity
const variance = prevVar;
const actualReturn = returns[t];
newLogLikelihood -= 0.5 * (Math.log(2 * Math.PI) + Math.log(variance) +
Math.pow(actualReturn - expectedReturn, 2) / variance);
}
// Check for convergence
if (Math.abs(newLogLikelihood - logLikelihood) < 1e-6) {
break;
}
logLikelihood = newLogLikelihood;
// Simple parameter update (in practice, use proper optimization)
kappa = Math.max(0.1, Math.min(10, kappa + 0.01));
theta = Math.max(0.001, Math.min(1, theta + 0.001));
sigma = Math.max(0.01, Math.min(2, sigma + 0.01));
rho = Math.max(-0.99, Math.min(0.99, rho + 0.01));
v0 = Math.max(0.001, Math.min(1, v0 + 0.001));
}
return {
kappa,
theta,
sigma,
rho,
v0,
logLikelihood
};
}
/**
* Calculate volatility risk metrics
*/
export function calculateVolatilityRisk(
returns: number[],
confidenceLevel: number = 0.05
): {
volatilityVaR: number;
expectedShortfall: number;
maxVolatility: number;
volatilityVolatility: number;
} {
// Calculate rolling volatilities
const windowSize = 30;
const volatilities: number[] = [];
for (let i = windowSize - 1; i < returns.length; i++) {
const window = returns.slice(i - windowSize + 1, i + 1);
const mean = window.reduce((sum, r) => sum + r, 0) / window.length;
const variance = window.reduce((sum, r) => sum + Math.pow(r - mean, 2), 0) / (window.length - 1);
volatilities.push(Math.sqrt(variance * 252)); // Annualized
}
// Sort volatilities for VaR calculation
const sortedVols = [...volatilities].sort((a, b) => b - a); // Descending order
const varIndex = Math.floor(confidenceLevel * sortedVols.length);
const volatilityVaR = sortedVols[varIndex];
// Expected shortfall (average of worst volatilities)
const esVols = sortedVols.slice(0, varIndex + 1);
const expectedShortfall = esVols.reduce((sum, vol) => sum + vol, 0) / esVols.length;
// Maximum volatility
const maxVolatility = Math.max(...volatilities);
// Volatility of volatility
const meanVol = volatilities.reduce((sum, vol) => sum + vol, 0) / volatilities.length;
const volVariance = volatilities.reduce((sum, vol) => sum + Math.pow(vol - meanVol, 2), 0) / (volatilities.length - 1);
const volatilityVolatility = Math.sqrt(volVariance);
return {
volatilityVaR,
expectedShortfall,
maxVolatility,
volatilityVolatility
};
}

View file

@ -1,67 +0,0 @@
/**
* Financial calculation utilities
*/
export const financialUtils = {
/**
* Calculate the Sharpe ratio
* @param returns Array of period returns
* @param riskFreeRate The risk-free rate (e.g. 0.02 for 2%)
*/
calculateSharpeRatio(returns: number[], riskFreeRate: number = 0.02): number {
if (returns.length < 2) {
return 0;
}
// Calculate average return
const avgReturn = returns.reduce((sum, val) => sum + val, 0) / returns.length;
// Calculate standard deviation
const squaredDiffs = returns.map(val => Math.pow(val - avgReturn, 2));
const avgSquaredDiff = squaredDiffs.reduce((sum, val) => sum + val, 0) / squaredDiffs.length;
const stdDev = Math.sqrt(avgSquaredDiff);
// Avoid division by zero
if (stdDev === 0) return 0;
// Calculate Sharpe ratio
return (avgReturn - riskFreeRate) / stdDev;
},
/**
* Calculate the maximum drawdown
* @param equityCurve Array of equity values over time
*/
calculateMaxDrawdown(equityCurve: number[]): number {
if (equityCurve.length < 2) {
return 0;
}
let maxDrawdown = 0;
let peak = equityCurve[0];
for (let i = 1; i < equityCurve.length; i++) {
if (equityCurve[i] > peak) {
peak = equityCurve[i];
} else {
const drawdown = (peak - equityCurve[i]) / peak;
maxDrawdown = Math.max(maxDrawdown, drawdown);
}
}
return maxDrawdown;
},
/**
* Calculate the Compound Annual Growth Rate (CAGR)
* @param startValue Initial investment value
* @param endValue Final investment value
* @param years Number of years
*/
calculateCAGR(startValue: number, endValue: number, years: number): number {
if (years <= 0 || startValue <= 0 || endValue <= 0) {
return 0;
}
return Math.pow(endValue / startValue, 1 / years) - 1;
}
};

View file

@ -1,4 +1,3 @@
export * from './dateUtils';
export * from './financialUtils';
export * from './logger';
export * from './lokiClient';