fitr
Version:
Financial Investment Tracker
138 lines (117 loc) • 5.62 kB
text/typescript
import yahooFinance from 'yahoo-finance2';
import { format, parse, startOfWeek, addDays, parseISO, isBefore, getDay, isEqual } from 'date-fns';
import { PricePoint, PriceSource } from '../types';
import { Logger } from '../utils/logger';
/**
* Get historical weekly price data for a symbol between two dates
*/
export async function getHistoricalPrices(
symbol: string,
fromDate: string,
toDate: string = format(new Date(), 'yyyy-MM-dd'),
interval: '1wk' | '1d' = '1wk'
): Promise<PricePoint[]> {
try {
Logger.info(`Fetching historical data for ${symbol} from ${fromDate} to ${toDate}...`);
const from = parse(fromDate, 'yyyy-MM-dd', new Date());
const to = parse(toDate, 'yyyy-MM-dd', new Date());
// Ensure period1 and period2 are not the same date
// If they are the same, add one day to period2
const adjustedTo = isEqual(from, to) ? addDays(to, 1) : to;
if (isEqual(from, to)) {
Logger.debug(`fromDate and toDate are the same (${fromDate}). Adjusting toDate to the next day.`);
}
// Use chart() instead of historical() as recommended by yahoo-finance2
const chartResult = await yahooFinance.chart(symbol, {
period1: from,
period2: adjustedTo,
interval: interval // Weekly or daily data
});
// Map the data to our PricePoint format
const pricePoints: PricePoint[] = chartResult.quotes
.filter(quote => quote.close !== null) // Filter out null values
.map(quote => ({
date: format(new Date(quote.date), 'yyyy-MM-dd'),
price: quote.close as number, // We've filtered out null values
source: PriceSource.YAHOO
}));
Logger.debug(`Found ${pricePoints.length} historical price points for ${symbol}`);
return pricePoints;
} catch (error) {
Logger.error(`Error fetching historical prices for ${symbol}`, error);
return [];
}
}
/**
* Get the next occurrence of a specific day of the week after a given date
* @param date The starting date
* @param dayOfWeek The day of the week to find (0 = Sunday, 1 = Monday, etc.)
* @returns The next occurrence of the specified day
*/
export function getNextDayOfWeek(date: Date, dayOfWeek: 0 | 1 | 2 | 3 | 4 | 5 | 6): Date {
const currentDay = getDay(date);
// Calculate days to add
let daysToAdd = dayOfWeek - currentDay;
if (daysToAdd <= 0) {
// If the day has already occurred this week, go to next week
daysToAdd += 7;
}
return addDays(date, daysToAdd);
}
/**
* Get weekly prices for a symbol since a given date
*
* @param symbol The stock/ETF symbol
* @param sinceDate The date to start fetching prices from (exclusive)
* @param weekStartsOn The day of the week to consider as the first day (0 = Sunday, 1 = Monday, etc.)
* @returns Array of price points
*/
export async function getWeeklyPrices(
symbol: string,
sinceDate: string,
weekStartsOn: 0 | 1 | 2 | 3 | 4 | 5 | 6 = 1 // Default to Monday
): Promise<PricePoint[]> {
try {
const today = new Date();
const sinceDateObj = parseISO(sinceDate);
// Debug logging
Logger.debug(`Getting weekly prices for ${symbol} since ${sinceDate}`);
Logger.debug(`Today: ${format(today, 'yyyy-MM-dd')}`);
Logger.debug(`Week starts on day: ${weekStartsOn} (0=Sunday, 1=Monday, ...)`);
// Get the start of the current week
const currentWeekStart = startOfWeek(today, { weekStartsOn });
Logger.debug(`Current week start: ${format(currentWeekStart, 'yyyy-MM-dd')}`);
// If the sinceDate is after or on the current week's start, no need to fetch new data
if (!isBefore(sinceDateObj, currentWeekStart)) {
Logger.info(`Last update for ${symbol} was already in the current week. No new data needed.`);
return [];
}
// Find the next occurrence of the specified weekday after sinceDate
const nextWeekday = getNextDayOfWeek(sinceDateObj, weekStartsOn);
Logger.debug(`Next ${weekStartsOn} after ${sinceDate} is ${format(nextWeekday, 'yyyy-MM-dd')}`);
// Format dates for API call
const fromDate = format(nextWeekday, 'yyyy-MM-dd');
const toDate = format(today, 'yyyy-MM-dd');
// Fetch historical prices
const allPrices = await getHistoricalPrices(symbol, fromDate, toDate, '1wk');
// Filter to only include prices for the specified day of the week
// or include all if we don't have enough data
if (allPrices.length > 7) {
const filteredPrices = allPrices.filter(price => {
const priceDate = parseISO(price.date);
return getDay(priceDate) === weekStartsOn;
});
// If we have filtered prices, return those, otherwise return all prices
if (filteredPrices.length > 0) {
Logger.debug(`Filtered to ${filteredPrices.length} prices for day ${weekStartsOn} of the week`);
return filteredPrices;
}
}
// If we don't have enough data or no filtered prices, return all prices
Logger.info(`Returning all ${allPrices.length} price points (not enough data to filter by day of week)`);
return allPrices;
} catch (error) {
Logger.error(`Error getting weekly prices for ${symbol}`, error);
return [];
}
}