617 lines
18 KiB
TypeScript
617 lines
18 KiB
TypeScript
/**
|
|
* 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;
|
|
|
|
if (n < 10) {
|
|
throw new Error('Need at least 10 observations for Heston parameter estimation');
|
|
}
|
|
|
|
// Initial parameter estimates
|
|
let kappa = 2.0; // Mean reversion speed
|
|
let theta = 0.04; // Long-term variance
|
|
let sigma = 0.3; // Volatility of variance
|
|
let rho = -0.5; // Correlation
|
|
let v0 = 0.04; // Initial variance
|
|
|
|
// Calculate sample statistics for initialization
|
|
const meanReturn = returns.reduce((sum, r) => sum + r, 0) / n;
|
|
const sampleVariance = returns.reduce((sum, r) => sum + Math.pow(r - meanReturn, 2), 0) / (n - 1);
|
|
|
|
theta = sampleVariance;
|
|
v0 = sampleVariance;
|
|
|
|
let logLikelihood = -Infinity;
|
|
|
|
for (let iter = 0; iter < maxIterations; iter++) {
|
|
let newLogLikelihood = 0;
|
|
let currentVariance = v0;
|
|
|
|
for (let t = 1; t < n; t++) {
|
|
const dt = 1.0; // Assuming daily data
|
|
const prevReturn = returns[t - 1];
|
|
|
|
// Euler discretization of variance process
|
|
const dW1 = Math.random() - 0.5; // Simplified random shock
|
|
const dW2 = rho * dW1 + Math.sqrt(1 - rho * rho) * (Math.random() - 0.5);
|
|
|
|
const varianceChange =
|
|
kappa * (theta - currentVariance) * dt +
|
|
sigma * Math.sqrt(Math.max(currentVariance, 0)) * dW2;
|
|
|
|
currentVariance = Math.max(currentVariance + varianceChange, 0.001);
|
|
|
|
// Log-likelihood contribution (simplified)
|
|
const expectedReturn = meanReturn;
|
|
const variance = currentVariance;
|
|
|
|
if (variance > 0) {
|
|
newLogLikelihood -= 0.5 * Math.log(2 * Math.PI * variance);
|
|
newLogLikelihood -= (0.5 * Math.pow(returns[t] - expectedReturn, 2)) / variance;
|
|
}
|
|
}
|
|
|
|
// Check for convergence
|
|
if (Math.abs(newLogLikelihood - logLikelihood) < 1e-6) {
|
|
break;
|
|
}
|
|
|
|
logLikelihood = newLogLikelihood;
|
|
|
|
// Simple parameter updates (in practice, use maximum likelihood estimation)
|
|
const learningRate = 0.001;
|
|
kappa = Math.max(0.1, Math.min(10, kappa + learningRate));
|
|
theta = Math.max(0.001, Math.min(1, theta + learningRate));
|
|
sigma = Math.max(0.01, Math.min(2, sigma + learningRate));
|
|
rho = Math.max(-0.99, Math.min(0.99, rho + learningRate * 0.1));
|
|
v0 = Math.max(0.001, Math.min(1, v0 + learningRate));
|
|
}
|
|
|
|
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,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Fix Yang-Zhang volatility calculation
|
|
*/
|
|
export function calculateYangZhangVolatility(
|
|
ohlcv: OHLCVData[],
|
|
annualizationFactor: number = 252
|
|
): number {
|
|
if (ohlcv.length < 2) {
|
|
throw new Error('Need at least 2 observations for Yang-Zhang volatility calculation');
|
|
}
|
|
|
|
const n = ohlcv.length;
|
|
let overnightSum = 0;
|
|
let openToCloseSum = 0;
|
|
let rogersSatchellSum = 0;
|
|
|
|
for (let i = 1; i < n; i++) {
|
|
const prev = ohlcv[i - 1];
|
|
const curr = ohlcv[i];
|
|
|
|
// Overnight return (close to open)
|
|
const overnight = Math.log(curr.open / prev.close);
|
|
overnightSum += overnight * overnight;
|
|
|
|
// Open to close return
|
|
const openToClose = Math.log(curr.close / curr.open);
|
|
openToCloseSum += openToClose * openToClose;
|
|
|
|
// Rogers-Satchell component
|
|
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
|
|
const k = 0.34 / (1.34 + (n + 1) / (n - 1)); // Drift adjustment factor
|
|
const yangZhangVariance =
|
|
overnightSum / (n - 1) +
|
|
(k * openToCloseSum) / (n - 1) +
|
|
((1 - k) * rogersSatchellSum) / (n - 1);
|
|
|
|
return Math.sqrt(yangZhangVariance * annualizationFactor);
|
|
}
|
|
|
|
/**
|
|
* Parkinson volatility estimator
|
|
*/
|
|
export function parkinsonVolatility(ohlcv: OHLCVData[], annualizationFactor: number = 252): number {
|
|
if (ohlcv.length < 2) {return 0;}
|
|
const sum = ohlcv.slice(1).reduce((acc, curr) => {
|
|
const range = Math.log(curr.high / curr.low);
|
|
return acc + range * range;
|
|
}, 0);
|
|
return Math.sqrt((sum / (ohlcv.length - 1)) * annualizationFactor);
|
|
}
|
|
|
|
/**
|
|
* Black-Scholes option pricing model
|
|
*/
|
|
function blackScholes(
|
|
spotPrice: number,
|
|
strikePrice: number,
|
|
timeToExpiry: number,
|
|
volatility: number,
|
|
riskFreeRate: number,
|
|
optionType: 'call' | 'put'
|
|
): number {
|
|
const d1 =
|
|
(Math.log(spotPrice / strikePrice) +
|
|
(riskFreeRate + 0.5 * volatility * volatility) * timeToExpiry) /
|
|
(volatility * Math.sqrt(timeToExpiry));
|
|
const d2 = d1 - volatility * Math.sqrt(timeToExpiry);
|
|
|
|
if (optionType === 'call') {
|
|
return (
|
|
spotPrice * normalCDF(d1) -
|
|
strikePrice * Math.exp(-riskFreeRate * timeToExpiry) * normalCDF(d2)
|
|
);
|
|
} else {
|
|
return (
|
|
strikePrice * Math.exp(-riskFreeRate * timeToExpiry) * normalCDF(-d2) -
|
|
spotPrice * normalCDF(-d1)
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Normal cumulative distribution function
|
|
*/
|
|
function normalCDF(x: number): number {
|
|
const a1 = 0.254829592;
|
|
const a2 = -0.284496736;
|
|
const a3 = 1.421060743;
|
|
const a4 = -1.453152027;
|
|
const a5 = 1.061405429;
|
|
const p = 0.3275911;
|
|
|
|
const sign = x < 0 ? -1 : 1;
|
|
const absX = Math.abs(x);
|
|
const t = 1 / (1 + p * absX);
|
|
const y =
|
|
1 -
|
|
(a1 * t + a2 * t * t + a3 * t * t * t + a4 * t * t * t * t + a5 * t * t * t * t * t) *
|
|
Math.exp((-absX * absX) / 2);
|
|
|
|
return 0.5 * (1 + sign * y);
|
|
}
|
|
|
|
/**
|
|
* Forecast volatility using EWMA
|
|
*/
|
|
export function forecastVolatilityEWMA(
|
|
volatilities: number[],
|
|
lambda: number = 0.94,
|
|
forecastHorizon: number = 1
|
|
): number {
|
|
if (volatilities.length === 0) {
|
|
return 0;
|
|
}
|
|
|
|
let forecast = volatilities[volatilities.length - 1];
|
|
for (let i = 0; i < forecastHorizon; i++) {
|
|
forecast = lambda * forecast + (1 - lambda) * forecast; // Using the last value as the long-term average
|
|
}
|
|
return forecast;
|
|
}
|