fed-policy-cli
Version:
Macro trading intelligence from Fed policy analysis. Transform economic data into actionable trading insights by analyzing historical Fed policy analogues.
238 lines (197 loc) • 7.58 kB
text/typescript
// /src/services/etfDataService.ts
import fetch from 'node-fetch';
export interface ETFDataPoint {
symbol: string;
name: string;
date: string;
open: number;
high: number;
low: number;
close: number;
volume: number;
}
export interface ETFFundamentals {
symbol: string;
name: string;
netAssets: number;
expenseRatio: number;
yieldPercent: number;
assetClass: string;
}
// Key ETFs for cross-asset Fed policy analysis
export const TARGET_ETFS = {
TLT: { name: '20+ Year Treasury Bond ETF', assetClass: 'Bonds' },
SPY: { name: 'S&P 500 ETF', assetClass: 'Equities' },
XLF: { name: 'Financial Sector ETF', assetClass: 'Equities' },
HYG: { name: 'High Yield Corporate Bond ETF', assetClass: 'Credit' },
LQD: { name: 'Investment Grade Corporate Bond ETF', assetClass: 'Credit' },
GLD: { name: 'Gold ETF', assetClass: 'Commodities' },
XLE: { name: 'Energy Sector ETF', assetClass: 'Equities' },
IWM: { name: 'Russell 2000 ETF', assetClass: 'Equities' }
};
const ALPHA_VANTAGE_BASE_URL = 'https://www.alphavantage.co/query';
/**
* Get Alpha Vantage API key from environment or CLI argument
*/
export const getAlphaVantageApiKey = (cliApiKey?: string): string => {
const apiKey = cliApiKey || process.env.ALPHA_VANTAGE_API_KEY || '';
if (!apiKey || apiKey === 'YOUR_ALPHA_VANTAGE_API_KEY_HERE') {
throw new Error(
'Alpha Vantage API key is required for ETF data. Please:\\n' +
'1. Get a free API key from: https://www.alphavantage.co/support/#api-key\\n' +
'2. Set it in a .env file: ALPHA_VANTAGE_API_KEY=your_key_here\\n' +
'3. Or use the --alpha-vantage-key flag'
);
}
return apiKey;
};
/**
* Fetch historical daily data for an ETF from Alpha Vantage
*/
export const fetchETFHistoricalData = async (
symbol: string,
apiKey: string,
outputSize: 'compact' | 'full' = 'full'
): Promise<ETFDataPoint[]> => {
const url = `${ALPHA_VANTAGE_BASE_URL}?function=TIME_SERIES_DAILY_ADJUSTED&symbol=${symbol}&outputsize=${outputSize}&apikey=${apiKey}`;
try {
const response = await fetch(url);
const data = await response.json() as any;
if (data['Error Message']) {
throw new Error(`Alpha Vantage error for ${symbol}: ${data['Error Message']}`);
}
if (data['Note']) {
throw new Error(`Alpha Vantage rate limit: ${data['Note']}`);
}
const timeSeries = data['Time Series (Daily)'];
if (!timeSeries) {
console.warn(`No time series data found for ${symbol}`);
return [];
}
const dataPoints: ETFDataPoint[] = [];
for (const [date, values] of Object.entries(timeSeries)) {
const point: ETFDataPoint = {
symbol,
name: TARGET_ETFS[symbol]?.name || symbol,
date,
open: parseFloat(values['1. open']),
high: parseFloat(values['2. high']),
low: parseFloat(values['3. low']),
close: parseFloat(values['5. adjusted close']), // Use adjusted close for accurate analysis
volume: parseInt(values['6. volume'])
};
dataPoints.push(point);
}
// Sort by date (oldest first)
return dataPoints.sort((a, b) => a.date.localeCompare(b.date));
} catch (error) {
console.error(`Error fetching ETF data for ${symbol}:`, error);
throw error;
}
};
/**
* Fetch ETF overview/fundamentals from Alpha Vantage
*/
export const fetchETFFundamentals = async (
symbol: string,
apiKey: string
): Promise<ETFFundamentals | null> => {
const url = `${ALPHA_VANTAGE_BASE_URL}?function=OVERVIEW&symbol=${symbol}&apikey=${apiKey}`;
try {
const response = await fetch(url);
const data = await response.json() as any;
if (data['Error Message'] || !data.Symbol) {
console.warn(`No fundamental data found for ${symbol}`);
return null;
}
return {
symbol: data.Symbol,
name: data.Name || TARGET_ETFS[symbol]?.name || symbol,
netAssets: parseFloat(data.TotalAssets) || 0,
expenseRatio: parseFloat(data.ExpenseRatio) || 0,
yieldPercent: parseFloat(data.DividendYield) || 0,
assetClass: TARGET_ETFS[symbol]?.assetClass || 'Unknown'
};
} catch (error) {
console.error(`Error fetching ETF fundamentals for ${symbol}:`, error);
return null;
}
};
/**
* Fetch all target ETF data with rate limiting
*/
export const fetchAllETFData = async (apiKey: string): Promise<{
historicalData: ETFDataPoint[];
fundamentals: ETFFundamentals[];
}> => {
const symbols = Object.keys(TARGET_ETFS);
const historicalData: ETFDataPoint[] = [];
const fundamentals: ETFFundamentals[] = [];
console.log(`Fetching data for ${symbols.length} ETFs...`);
for (let i = 0; i < symbols.length; i++) {
const symbol = symbols[i];
try {
console.log(`Fetching ${symbol} (${i + 1}/${symbols.length})...`);
// Fetch historical data
const historical = await fetchETFHistoricalData(symbol, apiKey, 'compact'); // Use compact for faster initial testing
historicalData.push(...historical);
// Add delay to respect rate limits (free tier: 500 calls/day, ~25 calls/hour)
if (i < symbols.length - 1) {
console.log('Waiting 15 seconds to respect API rate limits...');
await new Promise(resolve => setTimeout(resolve, 15000));
}
// Fetch fundamentals
const fundamental = await fetchETFFundamentals(symbol, apiKey);
if (fundamental) {
fundamentals.push(fundamental);
}
// Additional delay after fundamentals call
if (i < symbols.length - 1) {
console.log('Waiting 15 seconds to respect API rate limits...');
await new Promise(resolve => setTimeout(resolve, 15000));
}
} catch (error) {
console.error(`Failed to fetch data for ${symbol}:`, error);
// Continue with other symbols even if one fails
}
}
console.log(`Fetched ${historicalData.length} historical data points and ${fundamentals.length} fundamental records`);
return { historicalData, fundamentals };
};
/**
* Convert ETF daily data to monthly averages for consistency with economic data
*/
export const convertETFDataToMonthly = (dailyData: ETFDataPoint[]): ETFDataPoint[] => {
const monthlyData = new Map<string, ETFDataPoint[]>();
// Group by year-month
for (const point of dailyData) {
const yearMonth = point.date.substring(0, 7); // YYYY-MM
if (!monthlyData.has(yearMonth)) {
monthlyData.set(yearMonth, []);
}
monthlyData.get(yearMonth)!.push(point);
}
// Calculate monthly averages
const monthlyPoints: ETFDataPoint[] = [];
for (const [yearMonth, points] of monthlyData.entries()) {
if (points.length === 0) continue;
// Use last trading day's close as the monthly close
const lastPoint = points[points.length - 1];
// Calculate monthly averages
const avgOpen = points.reduce((sum, p) => sum + p.open, 0) / points.length;
const avgHigh = Math.max(...points.map(p => p.high));
const avgLow = Math.min(...points.map(p => p.low));
const totalVolume = points.reduce((sum, p) => sum + p.volume, 0);
monthlyPoints.push({
symbol: lastPoint.symbol,
name: lastPoint.name,
date: yearMonth + '-01', // Standardize to first of month
open: avgOpen,
high: avgHigh,
low: avgLow,
close: lastPoint.close, // Use actual month-end close
volume: totalVolume
});
}
return monthlyPoints.sort((a, b) => a.date.localeCompare(b.date));
};