added more functions
This commit is contained in:
parent
a1c82ae0b8
commit
cca9ac03dd
8 changed files with 1563 additions and 2 deletions
|
|
@ -233,3 +233,170 @@ export function modifiedDuration(
|
|||
const macDuration = macaulayDuration(faceValue, couponRate, yieldToMaturity, periodsToMaturity, paymentsPerYear);
|
||||
return macDuration / (1 + yieldToMaturity / paymentsPerYear);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate bond convexity
|
||||
*/
|
||||
export function bondConvexity(
|
||||
faceValue: number,
|
||||
couponRate: number,
|
||||
yieldToMaturity: number,
|
||||
periodsToMaturity: number,
|
||||
paymentsPerYear: number = 2
|
||||
): number {
|
||||
const couponPayment = (faceValue * couponRate) / paymentsPerYear;
|
||||
const discountRate = yieldToMaturity / paymentsPerYear;
|
||||
|
||||
let convexity = 0;
|
||||
const bondPriceValue = bondPrice(faceValue, couponRate, yieldToMaturity, periodsToMaturity, paymentsPerYear);
|
||||
|
||||
for (let i = 1; i <= periodsToMaturity; i++) {
|
||||
const presentValue = couponPayment / Math.pow(1 + discountRate, i);
|
||||
convexity += (i * (i + 1) * presentValue) / Math.pow(1 + discountRate, 2);
|
||||
}
|
||||
|
||||
const faceValuePV = faceValue / Math.pow(1 + discountRate, periodsToMaturity);
|
||||
convexity += (periodsToMaturity * (periodsToMaturity + 1) * faceValuePV) / Math.pow(1 + discountRate, 2);
|
||||
|
||||
return convexity / (bondPriceValue * paymentsPerYear * paymentsPerYear);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate dollar duration
|
||||
*/
|
||||
export function dollarDuration(
|
||||
faceValue: number,
|
||||
couponRate: number,
|
||||
yieldToMaturity: number,
|
||||
periodsToMaturity: number,
|
||||
paymentsPerYear: number = 2,
|
||||
basisPointChange: number = 0.01 // 1 basis point = 0.01%
|
||||
): number {
|
||||
const modifiedDur = modifiedDuration(faceValue, couponRate, yieldToMaturity, periodsToMaturity, paymentsPerYear);
|
||||
const bondPriceValue = bondPrice(faceValue, couponRate, yieldToMaturity, periodsToMaturity, paymentsPerYear);
|
||||
return modifiedDur * bondPriceValue * basisPointChange;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate accrued interest
|
||||
*/
|
||||
export function accruedInterest(
|
||||
faceValue: number,
|
||||
couponRate: number,
|
||||
daysSinceLastCoupon: number,
|
||||
daysInCouponPeriod: number
|
||||
): number {
|
||||
return (faceValue * couponRate) * (daysSinceLastCoupon / daysInCouponPeriod);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate clean price
|
||||
*/
|
||||
export function cleanPrice(dirtyPrice: number, accruedInterestValue: number): number {
|
||||
return dirtyPrice - accruedInterestValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate dirty price
|
||||
*/
|
||||
export function dirtyPrice(cleanPriceValue: number, accruedInterestValue: number): number {
|
||||
return cleanPriceValue + accruedInterestValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate dividend discount model (DDM)
|
||||
*/
|
||||
export function dividendDiscountModel(
|
||||
currentDividend: number,
|
||||
growthRate: number,
|
||||
discountRate: number
|
||||
): number {
|
||||
if (discountRate <= growthRate) return NaN; // Indeterminate
|
||||
return currentDividend * (1 + growthRate) / (discountRate - growthRate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate weighted average cost of capital (WACC)
|
||||
*/
|
||||
export function weightedAverageCostOfCapital(
|
||||
costOfEquity: number,
|
||||
costOfDebt: number,
|
||||
equityWeight: number,
|
||||
debtWeight: number,
|
||||
taxRate: number
|
||||
): number {
|
||||
return (equityWeight * costOfEquity) + (debtWeight * costOfDebt * (1 - taxRate));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate capital asset pricing model (CAPM)
|
||||
*/
|
||||
export function capitalAssetPricingModel(
|
||||
riskFreeRate: number,
|
||||
beta: number,
|
||||
marketRiskPremium: number
|
||||
): number {
|
||||
return riskFreeRate + beta * marketRiskPremium;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate Treynor ratio
|
||||
*/
|
||||
export function treynorRatio(
|
||||
portfolioReturn: number,
|
||||
riskFreeRate: number,
|
||||
beta: number
|
||||
): number {
|
||||
return (portfolioReturn - riskFreeRate) / beta;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate hurdle rate
|
||||
*/
|
||||
export function hurdleRate(
|
||||
costOfCapital: number,
|
||||
riskPremium: number
|
||||
): number {
|
||||
return costOfCapital + riskPremium;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate degree of operating leverage (DOL)
|
||||
*/
|
||||
export function degreeOfOperatingLeverage(
|
||||
contributionMargin: number,
|
||||
operatingIncome: number
|
||||
): number {
|
||||
return contributionMargin / operatingIncome;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate degree of financial leverage (DFL)
|
||||
*/
|
||||
export function degreeOfFinancialLeverage(
|
||||
ebit: number,
|
||||
earningsBeforeTax: number
|
||||
): number {
|
||||
return ebit / earningsBeforeTax;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate degree of total leverage (DTL)
|
||||
*/
|
||||
export function degreeOfTotalLeverage(
|
||||
dol: number,
|
||||
dfl: number
|
||||
): number {
|
||||
return dol * dfl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate economic value added (EVA)
|
||||
*/
|
||||
export function economicValueAdded(
|
||||
netOperatingProfitAfterTax: number,
|
||||
capitalInvested: number,
|
||||
wacc: number
|
||||
): number {
|
||||
return netOperatingProfitAfterTax - (capitalInvested * wacc);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -101,6 +101,8 @@ export function pearsonCorrelation(
|
|||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Calculate Spearman rank correlation coefficient
|
||||
*/
|
||||
|
|
@ -602,8 +604,276 @@ export function grangerCausalityTest(
|
|||
optimalLag: bestLag
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Calculate Distance Correlation
|
||||
*/
|
||||
export function distanceCorrelation(x: number[], y: number[]): CorrelationResult {
|
||||
if (x.length !== y.length || x.length < 2) {
|
||||
throw new Error('Arrays must have same length and at least 2 observations');
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
const n = x.length;
|
||||
|
||||
// Calculate distance matrices
|
||||
const a = Array(n).fill(null).map(() => Array(n).fill(0));
|
||||
const b = Array(n).fill(null).map(() => Array(n).fill(0));
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
for (let j = 0; j < n; j++) {
|
||||
a[i][j] = Math.abs(x[i] - x[j]);
|
||||
b[i][j] = Math.abs(y[i] - y[j]);
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate double centered distance matrices
|
||||
const aMeanRow = a.map(row => row.reduce((sum, val) => sum + val, 0) / n);
|
||||
const bMeanRow = b.map(row => row.reduce((sum, val) => sum + val, 0) / n);
|
||||
const aMeanTotal = aMeanRow.reduce((sum, val) => sum + val, 0) / n;
|
||||
const bMeanTotal = bMeanRow.reduce((sum, val) => sum + val, 0) / n;
|
||||
|
||||
const A = Array(n).fill(null).map(() => Array(n).fill(0));
|
||||
const B = Array(n).fill(null).map(() => Array(n).fill(0));
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
for (let j = 0; j < n; j++) {
|
||||
A[i][j] = a[i][j] - aMeanRow[i] - aMeanRow[j] + aMeanTotal;
|
||||
B[i][j] = b[i][j] - bMeanRow[i] - bMeanRow[j] + bMeanTotal;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate distance covariance and variances
|
||||
let dcov = 0;
|
||||
let dvarX = 0;
|
||||
let dvarY = 0;
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
for (let j = 0; j < n; j++) {
|
||||
dcov += A[i][j] * B[i][j];
|
||||
dvarX += A[i][j] * A[i][j];
|
||||
dvarY += B[i][j] * B[i][j];
|
||||
}
|
||||
}
|
||||
|
||||
dcov = Math.sqrt(dcov / (n * n));
|
||||
dvarX = Math.sqrt(dvarX / (n * n));
|
||||
dvarY = Math.sqrt(dvarY / (n * n));
|
||||
|
||||
const correlation = dvarX * dvarY === 0 ? 0 : dcov / Math.sqrt(dvarX * dvarY);
|
||||
|
||||
// Approximate p-value (permutation test)
|
||||
let pValue = 1;
|
||||
const numPermutations = 100;
|
||||
|
||||
for (let p = 0; p < numPermutations; p++) {
|
||||
const yPermuted = shuffleArray([...y]);
|
||||
const bPermuted = Array(n).fill(null).map(() => Array(n).fill(0));
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
for (let j = 0; j < n; j++) {
|
||||
bPermuted[i][j] = Math.abs(yPermuted[i] - yPermuted[j]);
|
||||
}
|
||||
}
|
||||
|
||||
const bMeanRowPermuted = bPermuted.map(row => row.reduce((sum, val) => sum + val, 0) / n);
|
||||
const bMeanTotalPermuted = bMeanRowPermuted.reduce((sum, val) => sum + val, 0) / n;
|
||||
|
||||
const BPermuted = Array(n).fill(null).map(() => Array(n).fill(0));
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
for (let j = 0; j < n; j++) {
|
||||
BPermuted[i][j] = bPermuted[i][j] - bMeanRowPermuted[i] - bMeanRowPermuted[j] + bMeanTotalPermuted;
|
||||
}
|
||||
}
|
||||
|
||||
let dcovPermuted = 0;
|
||||
for (let i = 0; i < n; i++) {
|
||||
for (let j = 0; j < n; j++) {
|
||||
dcovPermuted += A[i][j] * BPermuted[i][j];
|
||||
}
|
||||
}
|
||||
dcovPermuted = Math.sqrt(dcovPermuted / (n * n));
|
||||
|
||||
if (dcovPermuted >= dcov) {
|
||||
pValue++;
|
||||
}
|
||||
}
|
||||
|
||||
pValue /= (numPermutations + 1);
|
||||
const significance = pValue < 0.05;
|
||||
|
||||
return {
|
||||
correlation,
|
||||
pValue,
|
||||
significance
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate Mutual Information
|
||||
*/
|
||||
export function mutualInformation(x: number[], y: number[], numBins: number = 10): CorrelationResult {
|
||||
if (x.length !== y.length || x.length < 2) {
|
||||
throw new Error('Arrays must have same length and at least 2 observations');
|
||||
}
|
||||
|
||||
const n = x.length;
|
||||
|
||||
// Calculate histograms
|
||||
const xMin = Math.min(...x);
|
||||
const xMax = Math.max(...x);
|
||||
const yMin = Math.min(...y);
|
||||
const yMax = Math.max(...y);
|
||||
|
||||
const xBinWidth = (xMax - xMin) / numBins;
|
||||
const yBinWidth = (yMax - yMin) / numBins;
|
||||
|
||||
const jointHistogram = Array(numBins).fill(null).map(() => Array(numBins).fill(0));
|
||||
const xHistogram = Array(numBins).fill(0);
|
||||
const yHistogram = Array(numBins).fill(0);
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
const xBin = Math.floor((x[i] - xMin) / xBinWidth);
|
||||
const yBin = Math.floor((y[i] - yMin) / yBinWidth);
|
||||
|
||||
if (xBin >= 0 && xBin < numBins && yBin >= 0 && yBin < numBins) {
|
||||
jointHistogram[xBin][yBin]++;
|
||||
xHistogram[xBin]++;
|
||||
yHistogram[yBin]++;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate probabilities
|
||||
const jointProbabilities = jointHistogram.map(row => row.map(count => count / n));
|
||||
const xProbabilities = xHistogram.map(count => count / n);
|
||||
const yProbabilities = yHistogram.map(count => count / n);
|
||||
|
||||
// Calculate mutual information
|
||||
let mi = 0;
|
||||
for (let i = 0; i < numBins; i++) {
|
||||
for (let j = 0; j < numBins; j++) {
|
||||
if (jointProbabilities[i][j] > 0 && xProbabilities[i] > 0 && yProbabilities[j] > 0) {
|
||||
mi += jointProbabilities[i][j] * Math.log(jointProbabilities[i][j] / (xProbabilities[i] * yProbabilities[j]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const correlation = mi; // Use MI as correlation measure
|
||||
|
||||
// Approximate p-value (permutation test)
|
||||
let pValue = 1;
|
||||
const numPermutations = 100;
|
||||
|
||||
for (let p = 0; p < numPermutations; p++) {
|
||||
const yPermuted = shuffleArray([...y]);
|
||||
let miPermuted = 0;
|
||||
|
||||
const jointHistogramPermuted = Array(numBins).fill(null).map(() => Array(numBins).fill(0));
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
const xBin = Math.floor((x[i] - xMin) / xBinWidth);
|
||||
const yBin = Math.floor((yPermuted[i] - yMin) / yBinWidth);
|
||||
|
||||
if (xBin >= 0 && xBin < numBins && yBin >= 0 && yBin < numBins) {
|
||||
jointHistogramPermuted[xBin][yBin]++;
|
||||
}
|
||||
}
|
||||
|
||||
const jointProbabilitiesPermuted = jointHistogramPermuted.map(row => row.map(count => count / n));
|
||||
|
||||
for (let i = 0; i < numBins; i++) {
|
||||
for (let j = 0; j < numBins; j++) {
|
||||
if (jointProbabilitiesPermuted[i][j] > 0 && xProbabilities[i] > 0 && yProbabilities[j] > 0) {
|
||||
miPermuted += jointProbabilitiesPermuted[i][j] * Math.log(jointProbabilitiesPermuted[i][j] / (xProbabilities[i] * yProbabilities[j]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (miPermuted >= mi) {
|
||||
pValue++;
|
||||
}
|
||||
}
|
||||
|
||||
pValue /= (numPermutations + 1);
|
||||
const significance = pValue < 0.05;
|
||||
|
||||
return {
|
||||
correlation,
|
||||
pValue,
|
||||
significance
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate Cross-Correlation
|
||||
*/
|
||||
export function crossCorrelation(x: number[], y: number[], maxLag: number): number[] {
|
||||
const n = x.length;
|
||||
if (n !== y.length) {
|
||||
throw new Error('Arrays must have the same length');
|
||||
}
|
||||
|
||||
const correlations: number[] = [];
|
||||
|
||||
for (let lag = -maxLag; lag <= maxLag; lag++) {
|
||||
let sum = 0;
|
||||
let count = 0;
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
const yIndex = i + lag;
|
||||
|
||||
if (yIndex >= 0 && yIndex < n) {
|
||||
sum += (x[i] - average(x)) * (y[yIndex] - average(y));
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
const stdX = Math.sqrt(x.reduce((sum, xi) => sum + Math.pow(xi - average(x), 2), 0) / (n - 1));
|
||||
const stdY = Math.sqrt(y.reduce((sum, yi) => sum + Math.pow(yi - average(y), 2), 0) / (n - 1));
|
||||
|
||||
const correlation = count > 0 ? sum / ((count - 1) * stdX * stdY) : 0;
|
||||
correlations.push(correlation);
|
||||
}
|
||||
|
||||
return correlations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate Autocorrelation
|
||||
*/
|
||||
export function autocorrelation(x: number[], lag: number): number {
|
||||
const n = x.length;
|
||||
if (lag >= n) {
|
||||
throw new Error('Lag must be less than the length of the array');
|
||||
}
|
||||
|
||||
let sum = 0;
|
||||
for (let i = lag; i < n; i++) {
|
||||
sum += (x[i] - average(x)) * (x[i - lag] - average(x));
|
||||
}
|
||||
|
||||
const std = Math.sqrt(x.reduce((sum, xi) => sum + Math.pow(xi - average(x), 2), 0) / (n - 1));
|
||||
return sum / ((n - lag - 1) * std * std);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to shuffle an array (Fisher-Yates shuffle)
|
||||
*/
|
||||
function shuffleArray<T>(array: T[]): T[] {
|
||||
const newArray = [...array];
|
||||
for (let i = newArray.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[newArray[i], newArray[j]] = [newArray[j], newArray[i]];
|
||||
}
|
||||
return newArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to calculate the average of an array of numbers
|
||||
*/
|
||||
function average(arr: number[]): number {
|
||||
if (arr.length === 0) return 0;
|
||||
return arr.reduce((a, b) => a + b, 0) / arr.length;
|
||||
}
|
||||
|
||||
function getRanks(arr: number[]): number[] {
|
||||
const sorted = arr.map((val, idx) => ({ val, idx })).sort((a, b) => a.val - b.val);
|
||||
|
|
|
|||
|
|
@ -664,8 +664,293 @@ export function volumeConcentrationHHI(
|
|||
|
||||
return hhi * 10000; // Scale to 0-10000 range
|
||||
}
|
||||
/**
|
||||
* Volume Profile
|
||||
*/
|
||||
export function volumeProfile(
|
||||
ohlcv: OHLCVData[],
|
||||
priceLevels: number
|
||||
): { [price: number]: number } {
|
||||
const profile: { [price: number]: number } = {};
|
||||
|
||||
// Helper functions
|
||||
if (ohlcv.length === 0) return profile;
|
||||
|
||||
const minPrice = Math.min(...ohlcv.map(candle => candle.low));
|
||||
const maxPrice = Math.max(...ohlcv.map(candle => candle.high));
|
||||
const priceRange = maxPrice - minPrice;
|
||||
const priceIncrement = priceRange / priceLevels;
|
||||
|
||||
for (let i = 0; i < priceLevels; i++) {
|
||||
const priceLevel = minPrice + i * priceIncrement;
|
||||
profile[priceLevel] = 0;
|
||||
}
|
||||
|
||||
for (const candle of ohlcv) {
|
||||
const typicalPrice = (candle.high + candle.low + candle.close) / 3;
|
||||
const priceLevel = minPrice + Math.floor((typicalPrice - minPrice) / priceIncrement) * priceIncrement;
|
||||
if (profile[priceLevel] !== undefined) {
|
||||
profile[priceLevel] += candle.volume;
|
||||
}
|
||||
}
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delta Neutral Hedging Ratio
|
||||
*/
|
||||
export function deltaNeutralHedgingRatio(
|
||||
optionDelta: number
|
||||
): number {
|
||||
return -optionDelta;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gamma Scalping Range
|
||||
*/
|
||||
export function gammaScalpingRange(
|
||||
gamma: number,
|
||||
theta: number,
|
||||
timeIncrement: number
|
||||
): number {
|
||||
return Math.sqrt(2 * Math.abs(theta) * timeIncrement / gamma);
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimal Order Size (based on market impact)
|
||||
*/
|
||||
export function optimalOrderSize(
|
||||
alpha: number,
|
||||
lambda: number
|
||||
): number {
|
||||
return alpha / (2 * lambda);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adverse Selection Component of the Spread
|
||||
*/
|
||||
export function adverseSelectionComponent(
|
||||
probabilityOfInformedTrader: number,
|
||||
spread: number
|
||||
): number {
|
||||
return probabilityOfInformedTrader * spread;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inventory Risk Component of the Spread
|
||||
*/
|
||||
export function inventoryRiskComponent(
|
||||
inventoryHoldingCost: number,
|
||||
orderArrivalRate: number
|
||||
): number {
|
||||
return inventoryHoldingCost * Math.sqrt(orderArrivalRate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Quote Age
|
||||
*/
|
||||
export function quoteAge(
|
||||
lastUpdate: Date
|
||||
): number {
|
||||
return Date.now() - lastUpdate.getTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* Trade Classification (Lee-Ready algorithm)
|
||||
*/
|
||||
export function tradeClassification(
|
||||
tradePrice: number,
|
||||
bidPrice: number,
|
||||
askPrice: number,
|
||||
previousTradePrice: number
|
||||
): 'buy' | 'sell' | 'unknown' {
|
||||
if (tradePrice > askPrice) {
|
||||
return 'buy';
|
||||
} else if (tradePrice < bidPrice) {
|
||||
return 'sell';
|
||||
} else if (tradePrice >= previousTradePrice) {
|
||||
return 'buy';
|
||||
} else {
|
||||
return 'sell';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tick Rule
|
||||
*/
|
||||
export function tickRule(
|
||||
tradePrice: number,
|
||||
previousTradePrice: number
|
||||
): 'buy' | 'sell' | 'unknown' {
|
||||
if (tradePrice > previousTradePrice) {
|
||||
return 'buy';
|
||||
} else if (tradePrice < previousTradePrice) {
|
||||
return 'sell';
|
||||
} else {
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Amihud's Lambda Variation with High-Frequency Data
|
||||
*/
|
||||
export function amihudIlliquidityHFT(
|
||||
priceChanges: number[],
|
||||
dollarVolumes: number[],
|
||||
timeDeltas: number[]
|
||||
): number {
|
||||
let illiquiditySum = 0;
|
||||
let validTrades = 0;
|
||||
|
||||
for (let i = 0; i < priceChanges.length; i++) {
|
||||
if (dollarVolumes[i] > 0 && timeDeltas[i] > 0) {
|
||||
illiquiditySum += Math.abs(priceChanges[i]) / (dollarVolumes[i] * timeDeltas[i]);
|
||||
validTrades++;
|
||||
}
|
||||
}
|
||||
|
||||
return validTrades > 0 ? illiquiditySum / validTrades : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parkinson Volatility
|
||||
*/
|
||||
export function parkinsonVolatility(
|
||||
highPrices: number[],
|
||||
lowPrices: number[]
|
||||
): number {
|
||||
if (highPrices.length !== lowPrices.length || highPrices.length < 2) return 0;
|
||||
|
||||
let sumSquaredLogHL = 0;
|
||||
for (let i = 0; i < highPrices.length; i++) {
|
||||
const logHL = Math.log(highPrices[i] / lowPrices[i]);
|
||||
sumSquaredLogHL += logHL * logHL;
|
||||
}
|
||||
|
||||
const parkinsonVariance = (1 / (4 * highPrices.length * Math.log(2))) * sumSquaredLogHL;
|
||||
return Math.sqrt(parkinsonVariance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Garman-Klass Volatility
|
||||
*/
|
||||
export function garmanKlassVolatility(
|
||||
openPrices: number[],
|
||||
highPrices: number[],
|
||||
lowPrices: number[],
|
||||
closePrices: number[]
|
||||
): number {
|
||||
if (openPrices.length !== highPrices.length || openPrices.length !== lowPrices.length || openPrices.length !== closePrices.length || openPrices.length < 2) return 0;
|
||||
|
||||
let sumSquaredTerm1 = 0;
|
||||
let sumSquaredTerm2 = 0;
|
||||
let sumSquaredTerm3 = 0;
|
||||
|
||||
for (let i = 0; i < openPrices.length; i++) {
|
||||
const logHO = Math.log(highPrices[i] / openPrices[i]);
|
||||
const logLO = Math.log(lowPrices[i] / openPrices[i]);
|
||||
const logCO = Math.log(closePrices[i] / openPrices[i]);
|
||||
|
||||
sumSquaredTerm1 += 0.5 * (logHO * logHO + logLO * logLO);
|
||||
sumSquaredTerm2 += - (2 * Math.log(2) - 1) * (logCO * logCO);
|
||||
}
|
||||
|
||||
const garmanKlassVariance = (1 / openPrices.length) * (sumSquaredTerm1 + sumSquaredTerm2);
|
||||
return Math.sqrt(garmanKlassVariance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Yang-Zhang Volatility
|
||||
*/
|
||||
export function yangZhangVolatility(
|
||||
openPrices: number[],
|
||||
highPrices: number[],
|
||||
lowPrices: number[],
|
||||
closePrices: number[],
|
||||
previousClosePrices: number[]
|
||||
): number {
|
||||
if (openPrices.length !== highPrices.length || openPrices.length !== lowPrices.length || openPrices.length !== closePrices.length || openPrices.length !== previousClosePrices.length || openPrices.length < 2) return 0;
|
||||
|
||||
const k = 0.34 / (1.34 + (openPrices.length + 1) / (previousClosePrices.length - 1));
|
||||
|
||||
let sumSquaredTerm1 = 0;
|
||||
let sumSquaredTerm2 = 0;
|
||||
let sumSquaredTerm3 = 0;
|
||||
|
||||
for (let i = 0; i < openPrices.length; i++) {
|
||||
const overnightReturn = Math.log(openPrices[i] / previousClosePrices[i]);
|
||||
const openToHigh = Math.log(highPrices[i] / openPrices[i]);
|
||||
const openToLow = Math.log(lowPrices[i] / openPrices[i]);
|
||||
const closeToOpen = Math.log(closePrices[i] / openPrices[i]);
|
||||
|
||||
sumSquaredTerm1 += overnightReturn * overnightReturn;
|
||||
sumSquaredTerm2 += openToHigh * openToHigh;
|
||||
sumSquaredTerm3 += openToLow * openToLow;
|
||||
}
|
||||
|
||||
const variance = sumSquaredTerm1 + k * sumSquaredTerm2 + (1 - k) * sumSquaredTerm3;
|
||||
return Math.sqrt(variance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Volume Order Imbalance (VOI)
|
||||
*/
|
||||
export function volumeOrderImbalance(
|
||||
buyVolumes: number[],
|
||||
sellVolumes: number[]
|
||||
): number[] {
|
||||
if (buyVolumes.length !== sellVolumes.length) return [];
|
||||
|
||||
const voi: number[] = [];
|
||||
for (let i = 0; i < buyVolumes.length; i++) {
|
||||
voi.push(buyVolumes[i] - sellVolumes[i]);
|
||||
}
|
||||
return voi;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cumulative Volume Delta (CVD)
|
||||
*/
|
||||
export function cumulativeVolumeDelta(
|
||||
buyVolumes: number[],
|
||||
sellVolumes: number[]
|
||||
): number[] {
|
||||
if (buyVolumes.length !== sellVolumes.length) return [];
|
||||
|
||||
const cvd: number[] = [];
|
||||
let cumulativeDelta = 0;
|
||||
for (let i = 0; i < buyVolumes.length; i++) {
|
||||
cumulativeDelta += buyVolumes[i] - sellVolumes[i];
|
||||
cvd.push(cumulativeDelta);
|
||||
}
|
||||
return cvd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Market Order Ratio
|
||||
*/
|
||||
export function marketOrderRatio(
|
||||
marketOrders: number[],
|
||||
limitOrders: number[]
|
||||
): number[] {
|
||||
if (marketOrders.length !== limitOrders.length) return [];
|
||||
|
||||
const ratios: number[] = [];
|
||||
for (let i = 0; i < marketOrders.length; i++) {
|
||||
const totalOrders = marketOrders[i] + limitOrders[i];
|
||||
ratios.push(totalOrders > 0 ? marketOrders[i] / totalOrders : 0);
|
||||
}
|
||||
return ratios;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to calculate the average of an array of numbers
|
||||
*/
|
||||
|
||||
function average(arr: number[]): number {
|
||||
if (arr.length === 0) return 0;
|
||||
return arr.reduce((a, b) => a + b, 0) / arr.length;
|
||||
}
|
||||
|
||||
function calculateVolatility(returns: number[]): number {
|
||||
if (returns.length < 2) return 0;
|
||||
|
|
|
|||
|
|
@ -86,6 +86,58 @@ export function blackScholes(params: OptionParameters): OptionPricing {
|
|||
};
|
||||
}
|
||||
|
||||
export function impliedVolatility(
|
||||
price: number, S: number, K: number, T: number, r: number, isCall = true
|
||||
): number {
|
||||
// …Newton–Raphson on σ to match blackScholesPrice
|
||||
let sigma = 0.2; // Initial guess for volatility
|
||||
const tolerance = 1e-6;
|
||||
const maxIterations = 100;
|
||||
let iteration = 0;
|
||||
let priceDiff = 1; // Initialize to a non-zero value
|
||||
while (Math.abs(priceDiff) > tolerance && iteration < maxIterations) {
|
||||
const params: OptionParameters = {
|
||||
spotPrice: S,
|
||||
strikePrice: K,
|
||||
timeToExpiry: T,
|
||||
riskFreeRate: r,
|
||||
volatility: sigma
|
||||
};
|
||||
|
||||
const calculatedPrice = isCall ? blackScholes(params).callPrice : blackScholes(params).putPrice;
|
||||
priceDiff = calculatedPrice - price;
|
||||
|
||||
// Calculate Vega
|
||||
const greeks = calculateGreeks(params, isCall ? 'call' : 'put');
|
||||
const vega = greeks.vega * 100; // Convert from percentage to absolute
|
||||
|
||||
if (vega === 0) {
|
||||
break; // Avoid division by zero
|
||||
}
|
||||
|
||||
sigma -= priceDiff / vega; // Update volatility estimate
|
||||
iteration++;
|
||||
}
|
||||
if (iteration === maxIterations) {
|
||||
console.warn('Implied volatility calculation did not converge');
|
||||
}
|
||||
|
||||
if (sigma < 0) {
|
||||
console.warn('Calculated implied volatility is negative, returning 0');
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (sigma > 10) {
|
||||
console.warn('Calculated implied volatility is too high, returning 10');
|
||||
return 10; // Cap at a reasonable maximum
|
||||
}
|
||||
if (isNaN(sigma)) {
|
||||
console.warn('Calculated implied volatility is NaN, returning 0');
|
||||
return 0;
|
||||
}
|
||||
return sigma
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate option Greeks using Black-Scholes model
|
||||
*/
|
||||
|
|
@ -502,3 +554,165 @@ function boxMullerTransform(): number {
|
|||
|
||||
return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prices a straddle option strategy
|
||||
*/
|
||||
export function straddle(params: OptionParameters): { callPrice: number; putPrice: number; strategyCost: number } {
|
||||
const callOption = blackScholes(params);
|
||||
const putOption = blackScholes(params);
|
||||
const strategyCost = callOption.callPrice + putOption.putPrice;
|
||||
|
||||
return {
|
||||
callPrice: callOption.callPrice,
|
||||
putPrice: putOption.putPrice,
|
||||
strategyCost: strategyCost
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Prices a strangle option strategy
|
||||
*/
|
||||
export function strangle(callParams: OptionParameters, putParams: OptionParameters): { callPrice: number; putPrice: number; strategyCost: number } {
|
||||
const callOption = blackScholes(callParams);
|
||||
const putOption = blackScholes(putParams);
|
||||
const strategyCost = callOption.callPrice + putOption.putPrice;
|
||||
|
||||
return {
|
||||
callPrice: callOption.callPrice,
|
||||
putPrice: putOption.putPrice,
|
||||
strategyCost: strategyCost
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Prices a butterfly option strategy
|
||||
*/
|
||||
export function butterfly(
|
||||
lowerStrikeParams: OptionParameters,
|
||||
middleStrikeParams: OptionParameters,
|
||||
upperStrikeParams: OptionParameters
|
||||
): {
|
||||
lowerCallPrice: number;
|
||||
middleCallPrice: number;
|
||||
upperCallPrice: number;
|
||||
strategyCost: number;
|
||||
} {
|
||||
const lowerCall = blackScholes(lowerStrikeParams);
|
||||
const middleCall = blackScholes(middleStrikeParams);
|
||||
const upperCall = blackScholes(upperStrikeParams);
|
||||
|
||||
const strategyCost = lowerCall.callPrice - 2 * middleCall.callPrice + upperCall.callPrice;
|
||||
|
||||
return {
|
||||
lowerCallPrice: lowerCall.callPrice,
|
||||
middleCallPrice: middleCall.callPrice,
|
||||
upperCallPrice: upperCall.callPrice,
|
||||
strategyCost: strategyCost
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Prices a condor option strategy
|
||||
*/
|
||||
export function condor(
|
||||
lowerStrikeParams: OptionParameters,
|
||||
middleLowerStrikeParams: OptionParameters,
|
||||
middleUpperStrikeParams: OptionParameters,
|
||||
upperStrikeParams: OptionParameters
|
||||
): {
|
||||
lowerCallPrice: number;
|
||||
middleLowerCallPrice: number;
|
||||
middleUpperCallPrice: number;
|
||||
upperCallPrice: number;
|
||||
strategyCost: number;
|
||||
} {
|
||||
const lowerCall = blackScholes(lowerStrikeParams);
|
||||
const middleLowerCall = blackScholes(middleLowerStrikeParams);
|
||||
const middleUpperCall = blackScholes(middleUpperStrikeParams);
|
||||
const upperCall = blackScholes(upperStrikeParams);
|
||||
|
||||
const strategyCost = lowerCall.callPrice - middleLowerCall.callPrice - middleUpperCall.callPrice + upperCall.callPrice;
|
||||
|
||||
return {
|
||||
lowerCallPrice: lowerCall.callPrice,
|
||||
middleLowerCallPrice: middleLowerCall.callPrice,
|
||||
middleUpperCallPrice: middleUpperCall.callPrice,
|
||||
upperCallPrice: upperCall.callPrice,
|
||||
strategyCost: strategyCost
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates combined Greeks for an option strategy
|
||||
*/
|
||||
export function calculateStrategyGreeks(
|
||||
positions: Array<{
|
||||
optionType: 'call' | 'put';
|
||||
quantity: number;
|
||||
params: OptionParameters;
|
||||
}>
|
||||
): GreeksCalculation {
|
||||
let totalDelta = 0;
|
||||
let totalGamma = 0;
|
||||
let totalTheta = 0;
|
||||
let totalVega = 0;
|
||||
let totalRho = 0;
|
||||
|
||||
for (const position of positions) {
|
||||
const greeks = calculateGreeks(position.params, position.optionType);
|
||||
|
||||
totalDelta += greeks.delta * position.quantity;
|
||||
totalGamma += greeks.gamma * position.quantity;
|
||||
totalTheta += greeks.theta * position.quantity;
|
||||
totalVega += greeks.vega * position.quantity;
|
||||
totalRho += greeks.rho * position.quantity;
|
||||
}
|
||||
|
||||
return {
|
||||
delta: totalDelta,
|
||||
gamma: totalGamma,
|
||||
theta: totalTheta,
|
||||
vega: totalVega,
|
||||
rho: totalRho
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Black-Scholes option pricing model with greeks
|
||||
*/
|
||||
export function blackScholesWithGreeks(params: OptionParameters, optionType: 'call' | 'put' = 'call'): { pricing: OptionPricing; greeks: GreeksCalculation } {
|
||||
const pricing = blackScholes(params);
|
||||
const greeks = calculateGreeks(params, optionType);
|
||||
return { pricing, greeks };
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the breakeven point for a call option at expiration
|
||||
*/
|
||||
export function callBreakeven(strikePrice: number, callPrice: number): number {
|
||||
return strikePrice + callPrice;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the breakeven point for a put option at expiration
|
||||
*/
|
||||
export function putBreakeven(strikePrice: number, putPrice: number): number {
|
||||
return strikePrice - putPrice;
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimates the probability of profit for a call option at expiration
|
||||
*/
|
||||
export function callProbabilityOfProfit(spotPrice: number, strikePrice: number, timeToExpiry: number, riskFreeRate: number, volatility: number): number {
|
||||
const d1 = (Math.log(spotPrice / strikePrice) + (riskFreeRate + 0.5 * volatility * volatility) * timeToExpiry) / (volatility * Math.sqrt(timeToExpiry));
|
||||
return normalCDF(d1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimates the probability of profit for a put option at expiration
|
||||
*/
|
||||
export function putProbabilityOfProfit(spotPrice: number, strikePrice: number, timeToExpiry: number, riskFreeRate: number, volatility: number): number {
|
||||
const d1 = (Math.log(spotPrice / strikePrice) + (riskFreeRate + 0.5 * volatility * volatility) * timeToExpiry) / (volatility * Math.sqrt(timeToExpiry));
|
||||
return 1 - normalCDF(d1);
|
||||
}
|
||||
|
|
@ -463,6 +463,233 @@ export function calculateStrategyMetrics(
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate Calmar Ratio
|
||||
*/
|
||||
export function calmarRatio(returns: number[], equityCurve: Array<{ value: number; date: Date }>, riskFreeRate: number = 0): number {
|
||||
const maxDrawdown = analyzeDrawdowns(equityCurve).maxDrawdown;
|
||||
const avgReturn = returns.reduce((sum, ret) => sum + ret, 0) / returns.length;
|
||||
|
||||
return maxDrawdown === 0 ? 0 : (avgReturn - riskFreeRate) / maxDrawdown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate Sterling Ratio
|
||||
*/
|
||||
export function sterlingRatio(returns: number[], equityCurve: Array<{ value: number; date: Date }>, riskFreeRate: number = 0): number {
|
||||
const averageDrawdown = analyzeDrawdowns(equityCurve).averageDrawdown;
|
||||
const avgReturn = returns.reduce((sum, ret) => sum + ret, 0) / returns.length;
|
||||
|
||||
return averageDrawdown === 0 ? 0 : (avgReturn - riskFreeRate) / averageDrawdown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate Ulcer Index
|
||||
*/
|
||||
export function ulcerIndex(equityCurve: Array<{ value: number; date: Date }>): number {
|
||||
let sumSquaredDrawdown = 0;
|
||||
let peak = equityCurve[0].value;
|
||||
|
||||
for (const point of equityCurve) {
|
||||
peak = Math.max(peak, point.value);
|
||||
const drawdownPercent = (peak - point.value) / peak * 100;
|
||||
sumSquaredDrawdown += drawdownPercent * drawdownPercent;
|
||||
}
|
||||
|
||||
return Math.sqrt(sumSquaredDrawdown / equityCurve.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate Ulcer Performance Index (UPI)
|
||||
*/
|
||||
export function ulcerPerformanceIndex(returns: number[], equityCurve: Array<{ value: number; date: Date }>, riskFreeRate: number = 0): number {
|
||||
const ui = ulcerIndex(equityCurve);
|
||||
const avgReturn = returns.reduce((sum, ret) => sum + ret, 0) / returns.length;
|
||||
|
||||
return ui === 0 ? 0 : (avgReturn - riskFreeRate) / ui;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate Information Ratio
|
||||
*/
|
||||
export function informationRatio(portfolioReturns: number[], benchmarkReturns: number[]): number {
|
||||
if (portfolioReturns.length !== benchmarkReturns.length) {
|
||||
throw new Error("Portfolio and benchmark returns must have the same length.");
|
||||
}
|
||||
|
||||
const excessReturns = portfolioReturns.map((portfolioReturn, index) => portfolioReturn - benchmarkReturns[index]);
|
||||
const trackingError = calculateVolatility(excessReturns);
|
||||
const avgExcessReturn = excessReturns.reduce((sum, ret) => sum + ret, 0) / excessReturns.length;
|
||||
|
||||
return trackingError === 0 ? 0 : avgExcessReturn / trackingError;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate Treynor Ratio
|
||||
*/
|
||||
export function treynorRatio(portfolioReturns: number[], marketReturns: number[], riskFreeRate: number): number {
|
||||
const beta = calculateBeta(portfolioReturns, marketReturns);
|
||||
const avgPortfolioReturn = portfolioReturns.reduce((sum, ret) => sum + ret, 0) / portfolioReturns.length;
|
||||
|
||||
return beta === 0 ? 0 : (avgPortfolioReturn - riskFreeRate) / beta;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate Jensen's Alpha (same as Alpha, but included for clarity)
|
||||
*/
|
||||
export function jensensAlpha(portfolioReturns: number[], marketReturns: number[], riskFreeRate: number): number {
|
||||
return calculateAlpha(portfolioReturns, marketReturns, riskFreeRate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate Capture Ratio (Up Capture and Down Capture)
|
||||
*/
|
||||
export function captureRatio(portfolioReturns: number[], benchmarkReturns: number[]): { upCaptureRatio: number; downCaptureRatio: number } {
|
||||
let upCapture = 0;
|
||||
let downCapture = 0;
|
||||
let upMarketPeriods = 0;
|
||||
let downMarketPeriods = 0;
|
||||
|
||||
for (let i = 0; i < portfolioReturns.length; i++) {
|
||||
if (benchmarkReturns[i] > 0) {
|
||||
upCapture += portfolioReturns[i];
|
||||
upMarketPeriods++;
|
||||
} else if (benchmarkReturns[i] < 0) {
|
||||
downCapture += portfolioReturns[i];
|
||||
downMarketPeriods++;
|
||||
}
|
||||
}
|
||||
|
||||
const upCaptureRatio = upMarketPeriods > 0 ? (upCapture / upMarketPeriods) / (benchmarkReturns.filter(r => r > 0).reduce((sum, r) => sum + r, 0) / upMarketPeriods) : 0;
|
||||
const downCaptureRatio = downMarketPeriods > 0 ? (downCapture / downMarketPeriods) / (benchmarkReturns.filter(r => r < 0).reduce((sum, r) => sum + r, 0) / downMarketPeriods) : 0;
|
||||
|
||||
return { upCaptureRatio, downCaptureRatio };
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate Sortino Ratio
|
||||
*/
|
||||
export function sortinoRatio(returns: number[], riskFreeRate: number = 0): number {
|
||||
const avgReturn = returns.reduce((sum, ret) => sum + ret, 0) / returns.length;
|
||||
const downsideReturns = returns.filter(ret => ret < riskFreeRate);
|
||||
const downsideDeviation = Math.sqrt(
|
||||
downsideReturns.reduce((sum, ret) => sum + Math.pow(ret - riskFreeRate, 2), 0) / returns.length
|
||||
);
|
||||
|
||||
return downsideDeviation === 0 ? 0 : (avgReturn - riskFreeRate) / downsideDeviation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate Tail Ratio
|
||||
*/
|
||||
export function tailRatio(returns: number[], tailPercent: number = 0.1): number {
|
||||
const numReturns = returns.length;
|
||||
const tailSize = Math.floor(numReturns * tailPercent);
|
||||
|
||||
if (tailSize === 0) return 0;
|
||||
|
||||
const sortedReturns = [...returns].sort((a, b) => a - b);
|
||||
const worstTail = sortedReturns.slice(0, tailSize);
|
||||
const bestTail = sortedReturns.slice(numReturns - tailSize);
|
||||
|
||||
const avgWorst = worstTail.reduce((sum, ret) => sum + ret, 0) / tailSize;
|
||||
const avgBest = bestTail.reduce((sum, ret) => sum + ret, 0) / tailSize;
|
||||
|
||||
return avgWorst === 0 ? 0 : avgBest / Math.abs(avgWorst);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate Value at Risk (VaR)
|
||||
*/
|
||||
export function valueAtRisk(returns: number[], confidenceLevel: number = 0.05): number {
|
||||
const sortedReturns = [...returns].sort((a, b) => a - b);
|
||||
const varIndex = Math.floor(confidenceLevel * returns.length);
|
||||
return sortedReturns[varIndex];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate Conditional Value at Risk (CVaR) / Expected Shortfall
|
||||
*/
|
||||
export function conditionalValueAtRisk(returns: number[], confidenceLevel: number = 0.05): number {
|
||||
const sortedReturns = [...returns].sort((a, b) => a - b);
|
||||
const varIndex = Math.floor(confidenceLevel * returns.length);
|
||||
const tailReturns = sortedReturns.slice(0, varIndex + 1);
|
||||
return tailReturns.reduce((sum, ret) => sum + ret, 0) / tailReturns.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate Rolling Beta
|
||||
*/
|
||||
export function calculateRollingBeta(portfolioReturns: number[], marketReturns: number[], windowSize: number): number[] {
|
||||
if (portfolioReturns.length !== marketReturns.length || portfolioReturns.length < windowSize) return [];
|
||||
|
||||
const rollingBetas: number[] = [];
|
||||
|
||||
for (let i = windowSize; i <= portfolioReturns.length; i++) {
|
||||
const portfolioWindow = portfolioReturns.slice(i - windowSize, i);
|
||||
const marketWindow = marketReturns.slice(i - windowSize, i);
|
||||
rollingBetas.push(calculateBeta(portfolioWindow, marketWindow));
|
||||
}
|
||||
|
||||
return rollingBetas;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate Rolling Alpha
|
||||
*/
|
||||
export function calculateRollingAlpha(portfolioReturns: number[], marketReturns: number[], riskFreeRate: number, windowSize: number): number[] {
|
||||
if (portfolioReturns.length !== marketReturns.length || portfolioReturns.length < windowSize) return [];
|
||||
|
||||
const rollingAlphas: number[] = [];
|
||||
|
||||
for (let i = windowSize; i <= portfolioReturns.length; i++) {
|
||||
const portfolioWindow = portfolioReturns.slice(i - windowSize, i);
|
||||
const marketWindow = marketReturns.slice(i - windowSize, i);
|
||||
rollingAlphas.push(calculateAlpha(portfolioWindow, marketWindow, riskFreeRate));
|
||||
}
|
||||
|
||||
return rollingAlphas;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate Time Weighted Rate of Return (TWRR)
|
||||
*/
|
||||
export function timeWeightedRateOfReturn(cashFlows: Array<{ amount: number; date: Date; value: number }>): number {
|
||||
let totalReturn = 1;
|
||||
let previousValue = cashFlows[0].value;
|
||||
|
||||
for (let i = 1; i < cashFlows.length; i++) {
|
||||
const current = cashFlows[i];
|
||||
const periodReturn = (current.value - previousValue - current.amount) / (previousValue + current.amount);
|
||||
totalReturn *= (1 + periodReturn);
|
||||
previousValue = current.value;
|
||||
}
|
||||
|
||||
return totalReturn - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate Money Weighted Rate of Return (MWRR) - Approximation using IRR
|
||||
*/
|
||||
export function moneyWeightedRateOfReturn(cashFlows: Array<{ amount: number; date: Date; value: number }>): number {
|
||||
// Approximate MWRR using Internal Rate of Return (IRR)
|
||||
// This requires a numerical method or library for accurate IRR calculation
|
||||
// This is a simplified example and may not be accurate for all cases
|
||||
|
||||
let totalCashFlow = 0;
|
||||
let totalWeightedCashFlow = 0;
|
||||
const startDate = cashFlows[0].date.getTime();
|
||||
|
||||
for (const cf of cashFlows) {
|
||||
const timeDiff = (cf.date.getTime() - startDate) / (1000 * 60 * 60 * 24 * 365); // Years
|
||||
totalCashFlow += cf.amount;
|
||||
totalWeightedCashFlow += cf.amount * timeDiff;
|
||||
}
|
||||
|
||||
// Simplified approximation: MWRR ≈ totalCashFlow / totalWeightedCashFlow - 1
|
||||
return totalCashFlow / totalWeightedCashFlow - 1;
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
function calculateSharpeRatio(returns: number[], riskFreeRate: number = 0): number {
|
||||
|
|
|
|||
|
|
@ -547,3 +547,95 @@ export function jensenAlpha(
|
|||
const expectedReturn = capmExpectedReturn(riskFreeRate, marketReturn, portfolioBeta);
|
||||
return portfolioReturn - expectedReturn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate Ulcer Index
|
||||
*/
|
||||
export function ulcerIndex(equityCurve: number[]): number {
|
||||
if (equityCurve.length < 2) return 0;
|
||||
|
||||
let sumOfSquaredDrawdowns = 0;
|
||||
let peak = equityCurve[0];
|
||||
|
||||
for (let i = 1; i < equityCurve.length; i++) {
|
||||
if (equityCurve[i] > peak) {
|
||||
peak = equityCurve[i];
|
||||
}
|
||||
const drawdown = Math.max(0, (peak - equityCurve[i]) / peak);
|
||||
sumOfSquaredDrawdowns += drawdown * drawdown;
|
||||
}
|
||||
|
||||
return Math.sqrt(sumOfSquaredDrawdowns / equityCurve.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate Ulcer Performance Index (UPI)
|
||||
*/
|
||||
export function ulcerPerformanceIndex(returns: number[], equityCurve: number[], riskFreeRate: number = 0): number {
|
||||
const annualizedReturn = returns.reduce((sum, ret) => sum + ret, 0) / returns.length * 252; // Assuming daily returns
|
||||
const ui = ulcerIndex(equityCurve);
|
||||
|
||||
return ui !== 0 ? (annualizedReturn - riskFreeRate) / ui : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate Rachev Ratio
|
||||
*/
|
||||
export function rachevRatio(
|
||||
returns: number[],
|
||||
confidenceLevel: number = 0.05
|
||||
): number {
|
||||
if (returns.length === 0) return 0;
|
||||
|
||||
const sortedReturns = [...returns].sort((a, b) => a - b);
|
||||
const lossTailIndex = Math.floor(confidenceLevel * sortedReturns.length);
|
||||
const gainTailIndex = Math.floor((1 - confidenceLevel) * sortedReturns.length);
|
||||
|
||||
const expectedLoss = sortedReturns.slice(0, lossTailIndex)
|
||||
.reduce((sum, ret) => sum + ret, 0) / lossTailIndex;
|
||||
|
||||
const expectedGain = sortedReturns.slice(gainTailIndex)
|
||||
.reduce((sum, ret) => sum + ret, 0) / (sortedReturns.length - gainTailIndex);
|
||||
|
||||
return expectedGain > 0 && Math.abs(expectedLoss) > 0 ? expectedGain / Math.abs(expectedLoss) : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate Conditional Sharpe Ratio
|
||||
*/
|
||||
export function conditionalSharpeRatio(returns: number[], threshold: number, riskFreeRate: number = 0): number {
|
||||
const belowThresholdReturns = returns.filter(ret => ret <= threshold);
|
||||
|
||||
if (belowThresholdReturns.length < 2) return 0;
|
||||
|
||||
const mean = belowThresholdReturns.reduce((sum, ret) => sum + ret, 0) / belowThresholdReturns.length;
|
||||
const variance = belowThresholdReturns.reduce((sum, ret) => sum + Math.pow(ret - mean, 2), 0) / (belowThresholdReturns.length - 1);
|
||||
const stdDev = Math.sqrt(variance);
|
||||
|
||||
return stdDev !== 0 ? (mean - riskFreeRate) / stdDev : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate Adjusted Sharpe Ratio
|
||||
*/
|
||||
export function adjustedSharpeRatio(returns: number[], riskFreeRate: number = 0): number {
|
||||
const sr = sharpeRatio(returns, riskFreeRate);
|
||||
const sk = skewness(returns);
|
||||
const kurt = kurtosis(returns);
|
||||
|
||||
return sr * (1 + (sk / 6) * sr - ((kurt - 3) / 24) * sr * sr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate Bernardo-Ledoit Ratio
|
||||
*/
|
||||
export function bernardoLedoitRatio(returns: number[], riskFreeRate: number = 0): number {
|
||||
const excessReturns = returns.map(ret => ret - riskFreeRate);
|
||||
const positiveReturns = excessReturns.filter(ret => ret > 0);
|
||||
const negativeReturns = excessReturns.filter(ret => ret < 0);
|
||||
|
||||
const upsideMean = positiveReturns.length > 0 ? positiveReturns.reduce((sum, ret) => sum + ret, 0) / positiveReturns.length : 0;
|
||||
const downsideMean = negativeReturns.length > 0 ? negativeReturns.reduce((sum, ret) => sum + Math.abs(ret), 0) / negativeReturns.length : 0;
|
||||
|
||||
return downsideMean !== 0 ? upsideMean / downsideMean : 0;
|
||||
}
|
||||
|
|
@ -645,3 +645,196 @@ export function pivotPoints(ohlcv: OHLCVData[]): Array<{
|
|||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ichimoku Cloud
|
||||
*/
|
||||
export function ichimokuCloud(
|
||||
ohlcv: OHLCVData[],
|
||||
tenkanSenPeriod: number = 9,
|
||||
kijunSenPeriod: number = 26,
|
||||
senkouSpanBPeriod: number = 52
|
||||
): {
|
||||
tenkanSen: number[];
|
||||
kijunSen: number[];
|
||||
senkouSpanA: number[];
|
||||
senkouSpanB: number[];
|
||||
chikouSpan: number[];
|
||||
} {
|
||||
const { high, low, close } = {
|
||||
high: ohlcv.map(item => item.high),
|
||||
low: ohlcv.map(item => item.low),
|
||||
close: ohlcv.map(item => item.close)
|
||||
};
|
||||
|
||||
const tenkanSen = calculateTenkanSen(high, low, tenkanSenPeriod);
|
||||
const kijunSen = calculateKijunSen(high, low, kijunSenPeriod);
|
||||
const senkouSpanA = calculateSenkouSpanA(tenkanSen, kijunSen);
|
||||
const senkouSpanB = calculateSenkouSpanB(high, low, senkouSpanBPeriod);
|
||||
const chikouSpan = calculateChikouSpan(close, kijunSenPeriod);
|
||||
|
||||
return {
|
||||
tenkanSen,
|
||||
kijunSen,
|
||||
senkouSpanA,
|
||||
senkouSpanB,
|
||||
chikouSpan
|
||||
};
|
||||
|
||||
function calculateTenkanSen(high: number[], low: number[], period: number): number[] {
|
||||
const tenkanSen: number[] = [];
|
||||
for (let i = period - 1; i < high.length; i++) {
|
||||
const sliceHigh = high.slice(i - period + 1, i + 1);
|
||||
const sliceLow = low.slice(i - period + 1, i + 1);
|
||||
const highestHigh = Math.max(...sliceHigh);
|
||||
const lowestLow = Math.min(...sliceLow);
|
||||
tenkanSen.push((highestHigh + lowestLow) / 2);
|
||||
}
|
||||
return tenkanSen;
|
||||
}
|
||||
|
||||
function calculateKijunSen(high: number[], low: number[], period: number): number[] {
|
||||
const kijunSen: number[] = [];
|
||||
for (let i = period - 1; i < high.length; i++) {
|
||||
const sliceHigh = high.slice(i - period + 1, i + 1);
|
||||
const sliceLow = low.slice(i - period + 1, i + 1);
|
||||
const highestHigh = Math.max(...sliceHigh);
|
||||
const lowestLow = Math.min(...sliceLow);
|
||||
kijunSen.push((highestHigh + lowestLow) / 2);
|
||||
}
|
||||
return kijunSen;
|
||||
}
|
||||
|
||||
function calculateSenkouSpanA(tenkanSen: number[], kijunSen: number[]): number[] {
|
||||
const senkouSpanA: number[] = [];
|
||||
for (let i = 0; i < tenkanSen.length; i++) {
|
||||
senkouSpanA.push((tenkanSen[i] + kijunSen[i]) / 2);
|
||||
}
|
||||
return senkouSpanA;
|
||||
}
|
||||
|
||||
function calculateSenkouSpanB(high: number[], low: number[], period: number): number[] {
|
||||
const senkouSpanB: number[] = [];
|
||||
for (let i = period - 1; i < high.length; i++) {
|
||||
const sliceHigh = high.slice(i - period + 1, i + 1);
|
||||
const sliceLow = low.slice(i - period + 1, i + 1);
|
||||
const highestHigh = Math.max(...sliceHigh);
|
||||
const lowestLow = Math.min(...sliceLow);
|
||||
senkouSpanB.push((highestHigh + lowestLow) / 2);
|
||||
}
|
||||
return senkouSpanB;
|
||||
}
|
||||
|
||||
function calculateChikouSpan(close: number[], period: number): number[] {
|
||||
const chikouSpan: number[] = [];
|
||||
for (let i = 0; i < close.length - period; i++) {
|
||||
chikouSpan.push(close[i]);
|
||||
}
|
||||
return chikouSpan;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Keltner Channels
|
||||
*/
|
||||
export function keltnerChannels(
|
||||
ohlcv: OHLCVData[],
|
||||
period: number = 20,
|
||||
multiplier: number = 2
|
||||
): {
|
||||
upper: number[];
|
||||
middle: number[];
|
||||
lower: number[];
|
||||
} {
|
||||
const atrValues = atr(ohlcv, period);
|
||||
const middle = sma(ohlcv.map(item => (item.high + item.low + item.close) / 3), period);
|
||||
const upper: number[] = [];
|
||||
const lower: number[] = [];
|
||||
|
||||
for (let i = 0; i < middle.length; i++) {
|
||||
upper.push(middle[i] + multiplier * atrValues[i]);
|
||||
lower.push(middle[i] - multiplier * atrValues[i]);
|
||||
}
|
||||
|
||||
return {
|
||||
upper,
|
||||
middle,
|
||||
lower
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Donchian Channels
|
||||
*/
|
||||
export function donchianChannels(
|
||||
ohlcv: OHLCVData[],
|
||||
period: number = 20
|
||||
): {
|
||||
upper: number[];
|
||||
middle: number[];
|
||||
lower: number[];
|
||||
} {
|
||||
const upper: number[] = [];
|
||||
const lower: number[] = [];
|
||||
const middle: number[] = [];
|
||||
|
||||
for (let i = period - 1; i < ohlcv.length; i++) {
|
||||
const slice = ohlcv.slice(i - period + 1, i + 1);
|
||||
const highestHigh = Math.max(...slice.map(item => item.high));
|
||||
const lowestLow = Math.min(...slice.map(item => item.low));
|
||||
|
||||
upper.push(highestHigh);
|
||||
lower.push(lowestLow);
|
||||
middle.push((highestHigh + lowestLow) / 2);
|
||||
}
|
||||
|
||||
return {
|
||||
upper,
|
||||
middle,
|
||||
lower
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Elder-Ray Index
|
||||
*/
|
||||
export function elderRay(
|
||||
ohlcv: OHLCVData[],
|
||||
period: number = 13
|
||||
): {
|
||||
bullPower: number[];
|
||||
bearPower: number[];
|
||||
} {
|
||||
const emaValues = ema(ohlcv.map(item => item.close), period);
|
||||
const bullPower: number[] = [];
|
||||
const bearPower: number[] = [];
|
||||
|
||||
for (let i = period - 1; i < ohlcv.length; i++) {
|
||||
bullPower.push(ohlcv[i].high - emaValues[i - period + 1]);
|
||||
bearPower.push(ohlcv[i].low - emaValues[i - period + 1]);
|
||||
}
|
||||
|
||||
return {
|
||||
bullPower,
|
||||
bearPower
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Force Index
|
||||
*/
|
||||
export function forceIndex(
|
||||
ohlcv: OHLCVData[],
|
||||
period: number = 13
|
||||
): number[] {
|
||||
const forceIndexValues: number[] = [];
|
||||
|
||||
for (let i = 1; i < ohlcv.length; i++) {
|
||||
const change = ohlcv[i].close - ohlcv[i - 1].close;
|
||||
const volume = ohlcv[i].volume;
|
||||
forceIndexValues.push(change * volume);
|
||||
}
|
||||
|
||||
const smaValues = sma(forceIndexValues, period);
|
||||
return smaValues;
|
||||
}
|
||||
|
|
@ -517,3 +517,116 @@ export function calculateYangZhangVolatility(
|
|||
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate Implied Volatility using Black-Scholes model (simplified)
|
||||
*/
|
||||
export function calculateImpliedVolatility(
|
||||
optionPrice: number,
|
||||
spotPrice: number,
|
||||
strikePrice: number,
|
||||
timeToExpiry: number,
|
||||
riskFreeRate: number,
|
||||
optionType: 'call' | 'put',
|
||||
maxIterations: number = 100,
|
||||
tolerance: number = 1e-6
|
||||
): number {
|
||||
// Bisection method for implied volatility calculation
|
||||
let low = 0.01;
|
||||
let high = 5.0;
|
||||
let impliedVol = 0.0;
|
||||
|
||||
for (let i = 0; i < maxIterations; i++) {
|
||||
impliedVol = (low + high) / 2;
|
||||
const modelPrice = blackScholes(spotPrice, strikePrice, timeToExpiry, impliedVol, riskFreeRate, optionType);
|
||||
const diff = optionPrice - modelPrice;
|
||||
|
||||
if (Math.abs(diff) < tolerance) {
|
||||
return impliedVol;
|
||||
}
|
||||
|
||||
if (diff > 0) {
|
||||
low = impliedVol;
|
||||
} else {
|
||||
high = impliedVol;
|
||||
}
|
||||
}
|
||||
|
||||
return impliedVol; // Return best estimate if no convergence
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue