UNPKG

quantitivecalc

Version:

A TypeScript library providing advanced quantitative finance functions for risk analysis, performance metrics, and technical indicators. (Currently in development)

218 lines (214 loc) 8.61 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.calculatePeriodicCompoundReturns = calculatePeriodicCompoundReturns; exports.addPeriodicCompoundReturnsToData = addPeriodicCompoundReturnsToData; /** * Calculates compound returns grouped by time periods (yearly or monthly) from daily returns. * * Takes daily returns data and groups it by the specified time period, then calculates * compound returns for each period. The result includes the period identifier and * the compound return for that period. * * @param data - Array of objects representing the dataset. Each object should contain the returns column and date column. * @param returnsColumn - The key in each object representing the daily return value. Defaults to 'dailyReturn'. * @param dateColumn - The key in each object representing the date (timestamp in milliseconds or date string). Defaults to 'date'. * @param period - The time period to group by: 'yearly' or 'monthly'. Defaults to 'yearly'. * @param initialValue - The initial value to start compounding from. Defaults to 1. * @returns Array of objects with period identifier and compound return for each period. */ function calculatePeriodicCompoundReturns(data, returnsColumn = 'dailyReturn', dateColumn = 'date', period = 'yearly', initialValue = 1) { if (!data || data.length === 0) { return []; } // Sort data by date to ensure proper chronological order const sortedData = [...data].sort((a, b) => { const dateA = a[dateColumn]; const dateB = b[dateColumn]; let timestampA, timestampB; if (typeof dateA === 'number') { timestampA = dateA; } else if (typeof dateA === 'string') { timestampA = new Date(dateA).getTime(); } else { return 0; } if (typeof dateB === 'number') { timestampB = dateB; } else if (typeof dateB === 'string') { timestampB = new Date(dateB).getTime(); } else { return 0; } return timestampA - timestampB; }); // Group data by period const groupedData = new Map(); for (const row of sortedData) { const dateValue = row[dateColumn]; let date; if (typeof dateValue === 'number' && !Number.isNaN(dateValue)) { // Timestamp is already in milliseconds date = new Date(dateValue); } else if (typeof dateValue === 'string') { date = new Date(dateValue); if (Number.isNaN(date.getTime())) { continue; // Skip invalid date strings } } else { continue; // Skip invalid dates } let periodKey; if (period === 'yearly') { periodKey = date.getUTCFullYear().toString(); } else { // monthly const year = date.getUTCFullYear(); const month = (date.getUTCMonth() + 1).toString().padStart(2, '0'); periodKey = `${year}-${month}`; } if (!groupedData.has(periodKey)) { groupedData.set(periodKey, []); } groupedData.get(periodKey).push(row); } // Calculate compound returns for each period const result = []; for (const [periodKey, periodData] of groupedData) { if (periodData.length === 0) continue; let compoundValue = initialValue; // Get start and end dates for the period const startDateValue = periodData[0][dateColumn]; const endDateValue = periodData[periodData.length - 1][dateColumn]; let startDate, endDate; if (typeof startDateValue === 'number') { startDate = startDateValue; } else if (typeof startDateValue === 'string') { startDate = new Date(startDateValue).getTime(); } else { startDate = 0; } if (typeof endDateValue === 'number') { endDate = endDateValue; } else if (typeof endDateValue === 'string') { endDate = new Date(endDateValue).getTime(); } else { endDate = 0; } // Calculate compound return for this period for (const row of periodData) { const dailyReturn = row[returnsColumn]; if (typeof dailyReturn === 'number' && !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.push({ period: periodKey, compoundReturn: compoundValue - 1, // Convert back to return (subtract initial value) startDate, endDate, }); } // Sort results by period result.sort((a, b) => a.period.localeCompare(b.period)); return result; } /** * Alternative version that adds periodic compound returns back to the original dataset. * Each row gets the compound return for its corresponding period. * * @param data - Array of objects representing the dataset. * @param returnsColumn - The key in each object representing the daily return value. * @param dateColumn - The key in each object representing the date (timestamp in milliseconds or date string). * @param resultColumn - The key to store the calculated periodic compound return in each object. * @param period - The time period to group by: 'yearly' or 'monthly'. * @param initialValue - The initial value to start compounding from. * @returns A new array of objects with the periodic compound return added to each row. */ function addPeriodicCompoundReturnsToData(data, returnsColumn = 'dailyReturn', dateColumn = 'date', resultColumn, period = 'yearly', initialValue = 1) { if (!data || data.length === 0) { return []; } // First get the periodic compound returns const periodicReturns = calculatePeriodicCompoundReturns(data, returnsColumn, dateColumn, period, initialValue); // Create a map for quick lookup const returnsMap = new Map(); for (const item of periodicReturns) { returnsMap.set(item.period, item.compoundReturn); } // Create a copy of the data and add the periodic compound returns const result = data.map(row => { const newRow = { ...row }; const dateValue = row[dateColumn]; let date; if (typeof dateValue === 'number' && !Number.isNaN(dateValue)) { // Timestamp is already in milliseconds date = new Date(dateValue); } else if (typeof dateValue === 'string') { date = new Date(dateValue); if (Number.isNaN(date.getTime())) { newRow[resultColumn] = 0; return newRow; } } else { newRow[resultColumn] = 0; return newRow; } let periodKey; if (period === 'yearly') { periodKey = date.getUTCFullYear().toString(); } else { // monthly const year = date.getUTCFullYear(); const month = (date.getUTCMonth() + 1).toString().padStart(2, '0'); periodKey = `${year}-${month}`; } newRow[resultColumn] = returnsMap.get(periodKey) || 0; return newRow; }); return result; } // Example usage: /* const dailyData = [ { date: 1640995200000, dailyReturn: 0.01 }, // 2022-01-01 (milliseconds) { date: 1641081600000, dailyReturn: 0.02 }, // 2022-01-02 (milliseconds) { date: '2023-01-01', dailyReturn: 0.015 }, // 2023-01-01 (string) { date: '2023-01-02', dailyReturn: -0.01 }, // 2023-01-02 (string) ]; // Get yearly compound returns const yearlyReturns = calculatePeriodicCompoundReturns(dailyData, 'dailyReturn', 'date', 'yearly'); console.log(yearlyReturns); // Output: [ // { period: '2022', compoundReturn: 0.0302, startDate: 1640995200000, endDate: 1641081600000 }, // { period: '2023', compoundReturn: 0.004850, startDate: 1672531200000, endDate: 1672617600000 } // ] // Get monthly compound returns const monthlyReturns = calculatePeriodicCompoundReturns(dailyData, 'dailyReturn', 'date', 'monthly'); console.log(monthlyReturns); // Add yearly compound returns to original data const dataWithYearlyReturns = addPeriodicCompoundReturnsToData( dailyData, 'dailyReturn', 'date', 'yearlyCompoundReturn', 'yearly' ); console.log(dataWithYearlyReturns); */