stock-average-calculator
Version:
A comprehensive utility for calculating stock price averages and managing investment portfolios
325 lines (274 loc) • 10.8 kB
JavaScript
/**
* Stock Average Calculator
* A utility for calculating average stock prices and portfolio metrics
*/
/**
* Calculate the average purchase price of stocks
* @param {Array<Object>} transactions - Array of stock transactions
* @param {number} transactions[].shares - Number of shares in the transaction
* @param {number} transactions[].price - Price per share in the transaction
* @returns {Object} Object containing average price and other relevant metrics
*/
function calculateAveragePrice(transactions) {
if (!Array.isArray(transactions)) {
throw new Error('Transactions must be an array');
}
if (transactions.length === 0) {
throw new Error('At least one transaction is required');
}
let totalShares = 0;
let totalCost = 0;
transactions.forEach((transaction, index) => {
if (typeof transaction !== 'object' || transaction === null) {
throw new Error(`Transaction at index ${index} must be an object`);
}
const { shares, price } = transaction;
if (typeof shares !== 'number' || shares <= 0) {
throw new Error(`Shares at index ${index} must be a positive number`);
}
if (typeof price !== 'number' || price < 0) {
throw new Error(`Price at index ${index} must be a non-negative number`);
}
totalShares += shares;
totalCost += shares * price;
});
const averagePrice = totalCost / totalShares;
return {
totalShares,
totalCost: parseFloat(totalCost.toFixed(2)),
averagePrice: parseFloat(averagePrice.toFixed(2))
};
}
/**
* Calculate portfolio metrics with current market price
* @param {Array<Object>} transactions - Array of stock transactions
* @param {number} currentPrice - Current market price per share
* @returns {Object} Object containing portfolio metrics and profitability
*/
function calculatePortfolioMetrics(transactions, currentPrice) {
if (typeof currentPrice !== 'number' || currentPrice < 0) {
throw new Error('Current price must be a non-negative number');
}
const averagePriceResult = calculateAveragePrice(transactions);
const { totalShares, totalCost, averagePrice } = averagePriceResult;
const currentValue = totalShares * currentPrice;
const profitLoss = currentValue - totalCost;
const profitLossPercentage = (profitLoss / totalCost) * 100;
return {
...averagePriceResult,
currentPrice: parseFloat(currentPrice.toFixed(2)),
currentValue: parseFloat(currentValue.toFixed(2)),
profitLoss: parseFloat(profitLoss.toFixed(2)),
profitLossPercentage: parseFloat(profitLossPercentage.toFixed(2)),
isProfit: profitLoss >= 0
};
}
/**
* Calculate dollar cost averaging strategy results
* @param {number} initialInvestment - Initial investment amount
* @param {number} periodicInvestment - Amount invested periodically
* @param {Array<number>} prices - Array of stock prices over time
* @param {string} frequency - Investment frequency (e.g., 'monthly', 'weekly')
* @returns {Object} Object containing DCA results
*/
function calculateDollarCostAveraging(initialInvestment, periodicInvestment, prices, frequency = 'monthly') {
if (typeof initialInvestment !== 'number' || initialInvestment < 0) {
throw new Error('Initial investment must be a non-negative number');
}
if (typeof periodicInvestment !== 'number' || periodicInvestment < 0) {
throw new Error('Periodic investment must be a non-negative number');
}
if (!Array.isArray(prices) || prices.length === 0) {
throw new Error('Prices must be a non-empty array of numbers');
}
prices.forEach((price, index) => {
if (typeof price !== 'number' || price <= 0) {
throw new Error(`Price at index ${index} must be a positive number`);
}
});
const transactions = [];
// Initial investment
const initialShares = initialInvestment / prices[0];
transactions.push({ shares: initialShares, price: prices[0] });
// Periodic investments
for (let i = 1; i < prices.length; i++) {
const shares = periodicInvestment / prices[i];
transactions.push({ shares, price: prices[i] });
}
const lastPrice = prices[prices.length - 1];
const result = calculatePortfolioMetrics(transactions, lastPrice);
return {
...result,
frequency,
initialInvestment,
periodicInvestment,
periods: prices.length,
totalInvestment: initialInvestment + (periodicInvestment * (prices.length - 1))
};
}
/**
* Calculate the break-even price after averaging down
* @param {Array<Object>} transactions - Array of stock transactions
* @returns {number} Break-even price per share
*/
function calculateBreakEvenPrice(transactions) {
const { averagePrice } = calculateAveragePrice(transactions);
return averagePrice;
}
/**
* Format average price calculation result as a string
* @param {Object} result - The calculation result
* @returns {string} Formatted string with the calculation result
*/
function formatAveragePriceResult(result) {
if (!result) {
return 'Invalid calculation result';
}
return `
Average Price Calculation:
-------------------------
Total Shares: ${result.totalShares.toFixed(2)}
Total Cost: $${result.totalCost.toFixed(2)}
Average Price per Share: $${result.averagePrice.toFixed(2)}
`;
}
/**
* Format portfolio metrics result as a string
* @param {Object} result - The calculation result
* @returns {string} Formatted string with the calculation result
*/
function formatPortfolioMetricsResult(result) {
if (!result) {
return 'Invalid calculation result';
}
return `
Portfolio Metrics:
----------------
Total Shares: ${result.totalShares.toFixed(2)}
Average Price per Share: $${result.averagePrice.toFixed(2)}
Total Cost: $${result.totalCost.toFixed(2)}
Current Price per Share: $${result.currentPrice.toFixed(2)}
Current Portfolio Value: $${result.currentValue.toFixed(2)}
Profit/Loss: $${result.profitLoss.toFixed(2)} (${result.profitLossPercentage.toFixed(2)}%)
Status: ${result.isProfit ? 'PROFIT' : 'LOSS'}
`;
}
/**
* Format dollar cost averaging result as a string
* @param {Object} result - The calculation result
* @returns {string} Formatted string with the calculation result
*/
function formatDCAResult(result) {
if (!result) {
return 'Invalid calculation result';
}
return `
Dollar Cost Averaging Results:
----------------------------
Investment Frequency: ${result.frequency}
Initial Investment: $${result.initialInvestment.toFixed(2)}
Periodic Investment: $${result.periodicInvestment.toFixed(2)}
Total Periods: ${result.periods}
Total Investment: $${result.totalInvestment.toFixed(2)}
Total Shares Acquired: ${result.totalShares.toFixed(2)}
Average Price per Share: $${result.averagePrice.toFixed(2)}
Current Price per Share: $${result.currentPrice.toFixed(2)}
Current Portfolio Value: $${result.currentValue.toFixed(2)}
Profit/Loss: $${result.profitLoss.toFixed(2)} (${result.profitLossPercentage.toFixed(2)}%)
Status: ${result.isProfit ? 'PROFIT' : 'LOSS'}
`;
}
/**
* Quick calculate average price from simple inputs
* @param {Array<number>} amounts - Array of share amounts
* @param {Array<number>} prices - Array of corresponding prices
* @returns {Object} Average price calculation result
*/
function quickCalculateAverage(amounts, prices) {
if (!Array.isArray(amounts) || !Array.isArray(prices)) {
throw new Error('Amounts and prices must be arrays');
}
if (amounts.length !== prices.length) {
throw new Error('Amounts and prices arrays must have the same length');
}
if (amounts.length === 0) {
throw new Error('At least one transaction is required');
}
const transactions = amounts.map((shares, index) => ({
shares,
price: prices[index]
}));
return calculateAveragePrice(transactions);
}
/**
* Calculate optimal averaging strategy to reach target average
* @param {Array<Object>} currentTransactions - Current transactions
* @param {number} targetAveragePrice - Desired average price
* @param {number} currentMarketPrice - Current market price
* @returns {Object|null} Suggested transaction or null if target cannot be reached
*/
function calculateOptimalAveragingStrategy(currentTransactions, targetAveragePrice, currentMarketPrice) {
if (typeof targetAveragePrice !== 'number' || targetAveragePrice <= 0) {
throw new Error('Target average price must be a positive number');
}
if (typeof currentMarketPrice !== 'number' || currentMarketPrice <= 0) {
throw new Error('Current market price must be a positive number');
}
const { totalShares, totalCost, averagePrice } = calculateAveragePrice(currentTransactions);
// Check if the target is already achieved
if (Math.abs(averagePrice - targetAveragePrice) < 0.01) {
return {
currentAverage: averagePrice,
targetAverage: targetAveragePrice,
achievable: true,
message: 'Target average already achieved',
suggestedAction: null
};
}
// Cannot average up to a lower price or average down to a higher price
if ((averagePrice < targetAveragePrice && currentMarketPrice <= averagePrice) ||
(averagePrice > targetAveragePrice && currentMarketPrice >= averagePrice)) {
return {
currentAverage: averagePrice,
targetAverage: targetAveragePrice,
achievable: false,
message: `Cannot reach target average with current market price of $${currentMarketPrice}`
};
}
// Calculate required shares to reach target average
let requiredShares;
if (averagePrice > targetAveragePrice) {
// Averaging down
requiredShares = (totalCost - (targetAveragePrice * totalShares)) / (targetAveragePrice - currentMarketPrice);
} else {
// Averaging up
requiredShares = (targetAveragePrice * totalShares - totalCost) / (currentMarketPrice - targetAveragePrice);
}
requiredShares = Math.max(0, requiredShares);
const requiredInvestment = requiredShares * currentMarketPrice;
return {
currentAverage: averagePrice,
targetAverage: targetAveragePrice,
achievable: true,
suggestedAction: {
action: averagePrice > targetAveragePrice ? 'BUY' : 'SELL',
shares: parseFloat(requiredShares.toFixed(2)),
price: currentMarketPrice,
investment: parseFloat(requiredInvestment.toFixed(2))
},
newTotalShares: parseFloat((totalShares + requiredShares).toFixed(2)),
newTotalCost: parseFloat((totalCost + requiredInvestment).toFixed(2))
};
}
// Export functions
module.exports = {
calculateAveragePrice,
calculatePortfolioMetrics,
calculateDollarCostAveraging,
calculateBreakEvenPrice,
formatAveragePriceResult,
formatPortfolioMetricsResult,
formatDCAResult,
quickCalculateAverage,
calculateOptimalAveragingStrategy
};