fed-policy-cli
Version:
Macro trading intelligence from Fed policy analysis. Transform economic data into actionable trading insights by analyzing historical Fed policy analogues.
286 lines (243 loc) • 9.86 kB
text/typescript
// /src/services/api.ts
import fetch from 'node-fetch';
import { FRED_API_URL, getFredApiKey, FRED_SERIES, FOMC_PROJECTION_SERIES, CROSS_ASSET_SERIES } from '../constants';
import { EconomicDataPoint } from '../types';
import { FOMCProjection, CrossAssetDataPoint } from './database';
import { fetchAllETFData, getAlphaVantageApiKey } from './etfDataService';
interface FredObservation {
date: string;
value: string;
}
const fetchSeriesData = async (seriesId: string, apiKey: string): Promise<FredObservation[]> => {
const url = `${FRED_API_URL}?series_id=${seriesId}&api_key=${apiKey}&file_type=json`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch data for series ${seriesId}: ${response.statusText}`);
}
const data: any = await response.json();
return data.observations.map((obs: any) => ({ date: obs.date, value: obs.value }));
};
const transformData = (
seriesId: string,
observations: FredObservation[],
seriesSource: 'FRED' | 'CROSS_ASSET' = 'FRED'
): { [date: string]: number } => {
const seriesInfo = seriesSource === 'FRED' ? FRED_SERIES[seriesId] : CROSS_ASSET_SERIES[seriesId];
const { type } = seriesInfo;
const transformed: { [date: string]: number } = {};
const numericObservations = observations
.map(obs => ({ date: obs.date, value: parseFloat(obs.value) }))
.filter(obs => !isNaN(obs.value));
switch (type) {
case 'yoy':
case 'yoy_quarterly':
const valueMap: { [date: string]: number } = {};
numericObservations.forEach(point => {
valueMap[point.date] = point.value;
});
numericObservations.forEach(point => {
const [year, month, day] = point.date.split('-').map(Number);
const prevYearDate = `${year - 1}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
if (valueMap[prevYearDate]) {
const prevYearValue = valueMap[prevYearDate];
const yoy = ((point.value - prevYearValue) / prevYearValue) * 100;
transformed[point.date] = parseFloat(yoy.toFixed(2));
}
});
break;
case 'level':
default:
numericObservations.forEach(point => {
transformed[point.date] = point.value;
});
break;
}
return transformed;
};
export const fetchAllEconomicData = async (apiKey?: string): Promise<EconomicDataPoint[]> => {
const fredApiKey = getFredApiKey(apiKey);
const allSeriesData: { [seriesId: string]: { [date: string]: number } } = {};
// 1. Fetch and transform all series data in parallel
const seriesIds = Object.keys(FRED_SERIES);
const fetchPromises = seriesIds.map(async (id) => {
const observations = await fetchSeriesData(id, fredApiKey);
allSeriesData[id] = transformData(id, observations);
});
await Promise.all(fetchPromises);
// 2. Create a master set of all dates
const allDates = new Set<string>();
seriesIds.forEach(id => {
Object.keys(allSeriesData[id]).forEach(date => allDates.add(date));
});
const sortedDates = Array.from(allDates).sort();
// 3. Upsample all series to a daily frequency by forward-filling
const finalData: EconomicDataPoint[] = [];
const lastValues: { [seriesId: string]: number | null } = {};
seriesIds.forEach(id => (lastValues[id] = null));
for (const date of sortedDates) {
const dataPoint: EconomicDataPoint = { date };
for (const id of seriesIds) {
if (allSeriesData[id][date] != null) {
lastValues[id] = allSeriesData[id][date];
}
if (lastValues[id] !== null) {
dataPoint[id] = lastValues[id];
}
}
finalData.push(dataPoint);
}
return finalData;
};
// Fetch FOMC projection data (Fed dot plot)
export const fetchFOMCProjections = async (apiKey?: string): Promise<FOMCProjection[]> => {
const fredApiKey = getFredApiKey(apiKey);
const projections: FOMCProjection[] = [];
// Fetch all projection series
const seriesData: { [key: string]: FredObservation[] } = {};
const fetchPromises = Object.entries(FOMC_PROJECTION_SERIES).map(async ([seriesId, info]) => {
try {
const observations = await fetchSeriesData(seriesId, fredApiKey);
seriesData[seriesId] = observations;
} catch (error) {
console.warn(`Warning: Could not fetch ${info.name}: ${error.message}`);
seriesData[seriesId] = [];
}
});
await Promise.all(fetchPromises);
// Process the data to extract projections by meeting date and year
// FOMC projections are released quarterly and project rates for specific years
const projectionMap = new Map<string, FOMCProjection>();
// Helper to extract projection year from the series data
const processProjectionSeries = (seriesId: string, observations: FredObservation[]) => {
observations.forEach(obs => {
const meetingDate = obs.date;
const value = parseFloat(obs.value);
if (isNaN(value)) return;
// FOMC projections typically project for current year, next 2-3 years
// We'll need to parse the actual projection years from the data
// For now, we'll use a simple approach based on the meeting date
const meetingYear = parseInt(meetingDate.substring(0, 4));
// Create projections for current year + next 3 years
for (let yearOffset = 0; yearOffset <= 3; yearOffset++) {
const projectionYear = (meetingYear + yearOffset).toString();
const key = `${meetingDate}_${projectionYear}`;
if (!projectionMap.has(key)) {
projectionMap.set(key, {
meeting_date: meetingDate,
projection_year: projectionYear
});
}
const projection = projectionMap.get(key)!;
// Map the series to the appropriate field
switch (seriesId) {
case 'FEDTARMD':
projection.median_rate = value;
break;
case 'FEDTARRM':
projection.range_midpoint = value;
break;
case 'FEDTARRL':
projection.range_low = value;
break;
case 'FEDTARRH':
projection.range_high = value;
break;
case 'FEDTARMDLR':
projection.longer_run_median = value;
break;
}
}
});
};
// Process each series
Object.entries(seriesData).forEach(([seriesId, observations]) => {
if (observations.length > 0) {
processProjectionSeries(seriesId, observations);
}
});
// Convert map to array
projections.push(...projectionMap.values());
// Sort by meeting date and projection year
projections.sort((a, b) => {
const dateCompare = a.meeting_date.localeCompare(b.meeting_date);
if (dateCompare !== 0) return dateCompare;
return a.projection_year.localeCompare(b.projection_year);
});
return projections;
};
// Fetch cross-asset data (commodities, currencies, gold) from FRED
export const fetchAllCrossAssetData = async (apiKey?: string): Promise<CrossAssetDataPoint[]> => {
const fredApiKey = getFredApiKey(apiKey);
const allSeriesData: { [seriesId: string]: { [date: string]: number } } = {};
// 1. Fetch and transform all cross-asset series data in parallel
const seriesIds = Object.keys(CROSS_ASSET_SERIES);
console.log(`Fetching ${seriesIds.length} cross-asset series from FRED...`);
const fetchPromises = seriesIds.map(async (id) => {
try {
console.log(`Fetching ${id}: ${CROSS_ASSET_SERIES[id].name}`);
const observations = await fetchSeriesData(id, fredApiKey);
allSeriesData[id] = transformData(id, observations, 'CROSS_ASSET');
} catch (error) {
console.warn(`Warning: Could not fetch cross-asset series ${id}: ${error.message}`);
allSeriesData[id] = {};
}
});
await Promise.all(fetchPromises);
// 2. Create a master set of all dates
const allDates = new Set<string>();
seriesIds.forEach(id => {
Object.keys(allSeriesData[id]).forEach(date => allDates.add(date));
});
const sortedDates = Array.from(allDates).sort();
// 3. Upsample all series to a daily frequency by forward-filling
const finalData: CrossAssetDataPoint[] = [];
const lastValues: { [seriesId: string]: number | null } = {};
seriesIds.forEach(id => (lastValues[id] = null));
for (const date of sortedDates) {
const dataPoint: CrossAssetDataPoint = { date };
for (const id of seriesIds) {
if (allSeriesData[id][date] != null) {
lastValues[id] = allSeriesData[id][date];
}
if (lastValues[id] !== null) {
dataPoint[id] = lastValues[id];
}
}
finalData.push(dataPoint);
}
console.log(`Processed ${finalData.length} cross-asset data points`);
return finalData;
};
// Combined function to fetch both ETF and cross-asset data
export const fetchAllCrossAssetAndETFData = async (
fredApiKey?: string,
alphaVantageApiKey?: string
): Promise<{
crossAssetData: CrossAssetDataPoint[];
etfData: any[];
etfFundamentals: any[];
}> => {
console.log('Starting comprehensive cross-asset data fetch...');
// Fetch cross-asset FRED data
const crossAssetData = await fetchAllCrossAssetData(fredApiKey);
// Fetch ETF data if Alpha Vantage key is provided
let etfData: any[] = [];
let etfFundamentals: any[] = [];
if (alphaVantageApiKey) {
try {
console.log('Fetching ETF data from Alpha Vantage...');
const etfResult = await fetchAllETFData(alphaVantageApiKey);
etfData = etfResult.historicalData;
etfFundamentals = etfResult.fundamentals;
} catch (error) {
console.warn('Warning: Could not fetch ETF data from Alpha Vantage:', error.message);
}
} else {
console.log('No Alpha Vantage API key provided, skipping ETF data fetch');
}
return {
crossAssetData,
etfData,
etfFundamentals
};
};