add volitility-models
This commit is contained in:
parent
dda9f23285
commit
4397541d2c
4 changed files with 461 additions and 68 deletions
460
libs/utils/src/calculations/volatility-models.ts
Normal file
460
libs/utils/src/calculations/volatility-models.ts
Normal 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
|
||||
};
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
export * from './dateUtils';
|
||||
export * from './financialUtils';
|
||||
export * from './logger';
|
||||
export * from './lokiClient';
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue