quantitivecalc
Version:
A TypeScript library providing advanced quantitative finance functions for risk analysis, performance metrics, and technical indicators. (Currently in development)
282 lines (281 loc) • 12.5 kB
JavaScript
;
/**
* Performance Metrics Utilities
*
* Functions:
* - calculateCompoundReturns: Calculates compound values from a series of daily returns.
* - calculateDailyReturns: Calculates daily returns from a series of values.
* - calculateAnnualizedReturns: Converts returns to annualized format
* - calculateAlpha: Calculates alpha (excess return vs benchmark)
* - calculateInformationRatio: Active return per unit of tracking error
* - calculateCalmarRatio: Annual return divided by maximum drawdown
* - calculateSortinoRatio: Return adjusted for downside deviation
*
* All functions operate on arrays of objects (list of dicts) and allow you to specify source/result columns.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.calculateCompoundReturns = calculateCompoundReturns;
exports.calculateDailyReturns = calculateDailyReturns;
exports.calculateAnnualizedReturns = calculateAnnualizedReturns;
exports.calculateAlpha = calculateAlpha;
exports.calculateInformationRatio = calculateInformationRatio;
exports.calculateCalmarRatio = calculateCalmarRatio;
exports.calculateSortinoRatio = calculateSortinoRatio;
function calculateCompoundReturns(data, returnsColumn = 'dailyReturn', resultColumn, initialValue = 1) {
if (!data || data.length === 0) {
return [];
}
// Create a copy of the data to avoid mutating the original
const result = data.map(row => ({ ...row }));
let compoundValue = initialValue;
for (let i = 0; i < result.length; i++) {
const dailyReturn = result[i][returnsColumn];
if (typeof dailyReturn === 'number' && !isNaN(dailyReturn)) {
// Compound formula: previous_value * (1 + daily_return)
compoundValue = compoundValue * (1 + dailyReturn);
}
// If daily return is null/invalid, keep the same compound value
result[i][resultColumn] = compoundValue;
}
return result;
}
function calculateDailyReturns(data, sourceColumn, resultColumn = 'dailyReturn', initialValue = 0) {
if (!data || data.length === 0) {
return [];
}
// Create a copy of the data to avoid mutating the original
const result = data.map(row => ({ ...row }));
// Calculate daily returns for each row starting from index 1
for (let i = 0; i < result.length; i++) {
if (i === 0) {
// First row has no previous value, so daily return is null/undefined
result[i][resultColumn] = initialValue;
}
else {
const currentValue = result[i][sourceColumn];
const previousValue = result[i - 1][sourceColumn];
// Check if both values are valid numbers
if (typeof currentValue === 'number' &&
typeof previousValue === 'number' &&
previousValue !== 0 &&
!isNaN(currentValue) &&
!isNaN(previousValue)) {
// Daily return formula: (current - previous) / previous
const dailyReturn = (currentValue - previousValue) / previousValue;
result[i][resultColumn] = dailyReturn;
}
else {
// Invalid data - set to null
result[i][resultColumn] = null;
}
}
}
return result;
}
function calculateAnnualizedReturns(data, returnsColumn, resultColumn, frequency = 'daily', method = 'compound') {
if (!data || data.length === 0) {
return [];
}
const result = data.map(row => ({ ...row }));
// Periods per year based on frequency
const periodsPerYear = {
daily: 252, // Trading days
weekly: 52,
monthly: 12
};
const periods = periodsPerYear[frequency];
for (let i = 0; i < result.length; i++) {
const returnValue = result[i][returnsColumn];
if (typeof returnValue === 'number' && !isNaN(returnValue)) {
let annualizedReturn;
if (method === 'compound') {
// Compound: (1 + return)^periods - 1
annualizedReturn = Math.pow(1 + returnValue, periods) - 1;
}
else {
// Simple: return * periods
annualizedReturn = returnValue * periods;
}
result[i][resultColumn] = annualizedReturn;
}
else {
result[i][resultColumn] = null;
}
}
return result;
}
function calculateAlpha(data, assetReturnsColumn, benchmarkReturnsColumn, resultColumn, windowSize = 252, // 1 year for daily data
riskFreeRate = 0.02 // 2% annual
) {
if (!data || data.length === 0) {
return [];
}
const result = data.map(row => ({ ...row }));
const dailyRiskFreeRate = riskFreeRate / 252;
for (let i = 0; i < result.length; i++) {
if (i < windowSize - 1) {
result[i][resultColumn] = null;
}
else {
const assetReturns = [];
const benchmarkReturns = [];
// Collect returns for the window
for (let j = i - windowSize + 1; j <= i; j++) {
const assetReturn = result[j][assetReturnsColumn];
const benchmarkReturn = result[j][benchmarkReturnsColumn];
if (typeof assetReturn === 'number' && !isNaN(assetReturn) &&
typeof benchmarkReturn === 'number' && !isNaN(benchmarkReturn)) {
assetReturns.push(assetReturn - dailyRiskFreeRate);
benchmarkReturns.push(benchmarkReturn - dailyRiskFreeRate);
}
}
if (assetReturns.length > 10) { // Need sufficient data points
// Calculate beta (slope of regression)
const n = assetReturns.length;
const sumX = benchmarkReturns.reduce((a, b) => a + b, 0);
const sumY = assetReturns.reduce((a, b) => a + b, 0);
const sumXY = benchmarkReturns.reduce((sum, x, idx) => sum + x * assetReturns[idx], 0);
const sumXX = benchmarkReturns.reduce((sum, x) => sum + x * x, 0);
const beta = (n * sumXY - sumX * sumY) / (n * sumXX - sumX * sumX);
const avgAssetReturn = sumY / n;
const avgBenchmarkReturn = sumX / n;
// Alpha = Asset Return - Beta * Benchmark Return (annualized)
const alpha = (avgAssetReturn - beta * avgBenchmarkReturn) * 252;
result[i][resultColumn] = alpha;
}
else {
result[i][resultColumn] = null;
}
}
}
return result;
}
function calculateInformationRatio(data, assetReturnsColumn, benchmarkReturnsColumn, resultColumn, windowSize = 252) {
if (!data || data.length === 0) {
return [];
}
const result = data.map(row => ({ ...row }));
for (let i = 0; i < result.length; i++) {
if (i < windowSize - 1) {
result[i][resultColumn] = null;
}
else {
const activeReturns = [];
// Calculate active returns (asset - benchmark)
for (let j = i - windowSize + 1; j <= i; j++) {
const assetReturn = result[j][assetReturnsColumn];
const benchmarkReturn = result[j][benchmarkReturnsColumn];
if (typeof assetReturn === 'number' && !isNaN(assetReturn) &&
typeof benchmarkReturn === 'number' && !isNaN(benchmarkReturn)) {
activeReturns.push(assetReturn - benchmarkReturn);
}
}
if (activeReturns.length > 1) {
// Calculate mean and standard deviation of active returns
const meanActiveReturn = activeReturns.reduce((sum, val) => sum + val, 0) / activeReturns.length;
const variance = activeReturns.reduce((sum, val) => sum + Math.pow(val - meanActiveReturn, 2), 0) / (activeReturns.length - 1);
const trackingError = Math.sqrt(variance);
// Information Ratio = Mean Active Return / Tracking Error (annualized)
const informationRatio = trackingError > 0 ? (meanActiveReturn / trackingError) * Math.sqrt(252) : 0;
result[i][resultColumn] = informationRatio;
}
else {
result[i][resultColumn] = null;
}
}
}
return result;
}
function calculateCalmarRatio(data, returnsColumn, priceColumn, resultColumn, windowSize = 252) {
if (!data || data.length === 0) {
return [];
}
const result = data.map(row => ({ ...row }));
for (let i = 0; i < result.length; i++) {
if (i < windowSize - 1) {
result[i][resultColumn] = null;
}
else {
// Calculate annualized return for the window
const windowReturns = [];
for (let j = i - windowSize + 1; j <= i; j++) {
const returnValue = result[j][returnsColumn];
if (typeof returnValue === 'number' && !isNaN(returnValue)) {
windowReturns.push(returnValue);
}
}
// Calculate maximum drawdown for the window
let peak = -Infinity;
let maxDrawdown = 0;
for (let j = i - windowSize + 1; j <= i; j++) {
const price = result[j][priceColumn];
if (typeof price === 'number' && !isNaN(price)) {
if (price > peak)
peak = price;
const drawdown = (peak - price) / peak;
if (drawdown > maxDrawdown)
maxDrawdown = drawdown;
}
}
if (windowReturns.length > 0 && maxDrawdown > 0) {
// Annualized return
const avgReturn = windowReturns.reduce((sum, val) => sum + val, 0) / windowReturns.length;
const annualizedReturn = avgReturn * 252;
// Calmar Ratio = Annualized Return / Maximum Drawdown
const calmarRatio = annualizedReturn / maxDrawdown;
result[i][resultColumn] = calmarRatio;
}
else {
result[i][resultColumn] = null;
}
}
}
return result;
}
function calculateSortinoRatio(data, returnsColumn, resultColumn, windowSize = 252, riskFreeRate = 0.02, targetReturn = null // If null, uses risk-free rate
) {
if (!data || data.length === 0) {
return [];
}
const result = data.map(row => ({ ...row }));
const dailyRiskFreeRate = riskFreeRate / 252;
const dailyTargetReturn = targetReturn ? targetReturn / 252 : dailyRiskFreeRate;
for (let i = 0; i < result.length; i++) {
if (i < windowSize - 1) {
result[i][resultColumn] = null;
}
else {
const windowReturns = [];
const downsideReturns = [];
// Collect returns and downside returns
for (let j = i - windowSize + 1; j <= i; j++) {
const returnValue = result[j][returnsColumn];
if (typeof returnValue === 'number' && !isNaN(returnValue)) {
windowReturns.push(returnValue);
// Only include returns below target for downside deviation
if (returnValue < dailyTargetReturn) {
downsideReturns.push(returnValue - dailyTargetReturn);
}
}
}
if (windowReturns.length > 1) {
// Calculate mean return
const meanReturn = windowReturns.reduce((sum, val) => sum + val, 0) / windowReturns.length;
// Calculate downside deviation
let downsideDeviation = 0;
if (downsideReturns.length > 0) {
const downsideVariance = downsideReturns.reduce((sum, val) => sum + val * val, 0) / windowReturns.length;
downsideDeviation = Math.sqrt(downsideVariance);
}
// Sortino Ratio = (Mean Return - Target Return) / Downside Deviation (annualized)
const excessReturn = meanReturn - dailyTargetReturn;
const sortinoRatio = downsideDeviation > 0 ? (excessReturn / downsideDeviation) * Math.sqrt(252) : 0;
result[i][resultColumn] = sortinoRatio;
}
else {
result[i][resultColumn] = null;
}
}
}
return result;
}