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
JavaScript
;
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);
*/