UNPKG

fitr

Version:
176 lines (146 loc) 7.32 kB
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(); } }