fitr
Version:
Financial Investment Tracker
176 lines (146 loc) • 7.32 kB
text/typescript
import {format, isToday, parseISO, startOfWeek, subWeeks} from 'date-fns';
import {getPortfolio, updatePortfolio} from '../services/portfolioService';
import {getTransactions} from '../services/transactionService';
import {savePriceHistory, getPriceHistory} from '../services/priceHistoryService';
import {fetchAndSaveCurrencyExchangeRate} from '../services/currencyService';
import {
getHistoricalPrices,
getWeeklyPrices
} from '../services/yahooFinance';
import {formatDate} from '../utils/dateUtils';
import {PricePoint, Transaction} from "../types";
import {Logger} from '../utils/logger';
/**
* Update historical price data for a symbol
* @param symbol The stock/ETF symbol to update
* @param weekStartsOn The day of the week to consider as the first day (0 = Sunday, 1 = Monday, etc.)
* @returns Promise that resolves when the update is complete
*/
export async function updateHistoricalPricesDataForSymbol(
symbol: string,
weekStartsOn: 0 | 1 | 2 | 3 | 4 | 5 | 6 = 1
): Promise<void> {
Logger.start(`Updating historical data for ${symbol}...`);
try {
// Get existing price history
const existingPriceHistory = await getPriceHistory(symbol);
// If no price history exists, initialize it from the first transaction date
if (existingPriceHistory.length === 0) {
Logger.info(`No existing price data for ${symbol}. Will fetch initial data.`);
// Get the first transaction date for this asset
const transactions = await getTransactions(symbol);
if (transactions.length === 0) {
Logger.warn(`No transactions found for ${symbol}. Skipping price history update.`);
return;
}
// Find the first transaction date
const firstTransaction = [...transactions]
.sort((a: Transaction, b: Transaction) =>
new Date(a.date).getTime() - new Date(b.date).getTime()
)[0];
// Fetch historical prices from the first transaction date
const pricePoints = await getHistoricalPrices(symbol, firstTransaction.date);
Logger.info(`Fetched ${pricePoints.length} initial price points for ${symbol}`);
// Save the price history
await savePriceHistory(symbol, pricePoints);
}
// If price history exists, update it with new data if needed
else {
// Find the latest price date
const sortedPriceHistory = [...existingPriceHistory].sort(
(a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()
);
const latestPricePoint = sortedPriceHistory[0];
const latestPriceDate = parseISO(latestPricePoint.date);
Logger.info(`Latest price for ${symbol} is from ${formatDate(latestPricePoint.date)}`);
// Get the start of the current week
const today = new Date();
const currentWeekStart = startOfWeek(today, { weekStartsOn });
// Get the start of the previous week
const previousWeekStart = startOfWeek(subWeeks(today, 1), { weekStartsOn });
// Debug logging
Logger.debug(`Today: ${format(today, 'yyyy-MM-dd')}`);
Logger.debug(`Latest price date: ${format(latestPriceDate, 'yyyy-MM-dd')}`);
Logger.debug(`Current week start: ${format(currentWeekStart, 'yyyy-MM-dd')}`);
Logger.debug(`Previous week start: ${format(previousWeekStart, 'yyyy-MM-dd')}`);
Logger.debug(`Week starts on day: ${weekStartsOn} (0=Sunday, 1=Monday, ...)`);
// Check if we already have today's price
const hasToday = sortedPriceHistory.some(price => isToday(parseISO(price.date)));
if (hasToday) {
Logger.info(`Already have today's price for ${symbol}. Skipping update.`);
return;
}
// Fetch new weekly prices
const newPricePoints = await getWeeklyPrices(symbol, latestPricePoint.date, weekStartsOn);
Logger.debug(`Fetched ${newPricePoints.length} new price points for ${symbol}`);
if (newPricePoints.length === 0) {
Logger.debug(`No new price points for ${symbol}. Skipping update.`);
return;
}
// Merge existing and new price points, avoiding duplicates
const mergedPriceHistory = mergePriceHistories(existingPriceHistory, newPricePoints);
// Save the updated price history
await savePriceHistory(symbol, mergedPriceHistory);
Logger.info(`Updated price history for ${symbol} with ${newPricePoints.length} new points`);
}
} catch (error) {
Logger.error(`Error updating price history for ${symbol}`, error);
} finally {
Logger.end();
}
}
/**
* Merge two price histories, avoiding duplicates
* @param existingHistory Existing price history
* @param newHistory New price history to merge
* @returns Merged price history without duplicates
*/
export function mergePriceHistories(existingHistory: PricePoint[], newHistory: PricePoint[]): PricePoint[] {
// Create a map of existing prices by date for quick lookup
const existingPricesByDate = new Map<string, PricePoint>();
existingHistory.forEach(price => {
existingPricesByDate.set(price.date, price);
});
// Add new prices only if they don't already exist
for (const newPrice of newHistory) {
if (!existingPricesByDate.has(newPrice.date)) {
existingPricesByDate.set(newPrice.date, newPrice);
}
}
// Convert the map back to an array
return Array.from(existingPricesByDate.values());
}
/**
* Update historical price data for all assets in the portfolio
* @param weekStartsOn The day of the week to consider as the first day (0 = Sunday, 1 = Monday, etc.)
*/
export async function updateData(weekStartsOn: 0 | 1 | 2 | 3 | 4 | 5 | 6 = 1): Promise<void> {
Logger.start('Updating portfolio data');
try {
const portfolio = await getPortfolio();
if (portfolio.assets.length === 0) {
Logger.warn('Portfolio is empty. Add some assets first?');
return;
}
const symbols = portfolio.assets.map(asset => asset.symbol);
for (const symbol of symbols) {
await updateHistoricalPricesDataForSymbol(symbol, weekStartsOn);
}
Logger.start(`Updating currency exchange rates...`);
const currencies = [...new Set(portfolio.assets.map(asset => asset.currency))];
for (const c1 of currencies) {
for (const c2 of currencies) {
if(c1 !== c2) {
await fetchAndSaveCurrencyExchangeRate(c1, c2)
}
}
}
Logger.end();
await updatePortfolio();
Logger.info(`Data updated successfully on ${format(new Date(), 'MMMM do, yyyy')}`);
} catch (error) {
Logger.error('Error updating data', error);
} finally {
Logger.end();
}
}