UNPKG

fed-policy-cli

Version:

Macro trading intelligence from Fed policy analysis. Transform economic data into actionable trading insights by analyzing historical Fed policy analogues.

359 lines (311 loc) 11.4 kB
import { HistoricalAnalogue, FedPolicyAction, EconomicDataPoint } from '../types'; export interface PolicyRegime { type: 'easing' | 'tightening' | 'neutral' | 'mixed'; startDate: string; endDate: string; totalChange: number; duration: number; context: string; } export interface PolicyProjection { month: string; action: 'HIKE' | 'CUT' | 'HOLD'; probability: number; basisPoints?: number; rationale: string; } export interface PolicyPlaybook { pattern: string; keyTriggers: string[]; typicalSequence: string[]; expectedDuration: number; historicalSuccess: number; } /** * Identify policy regimes within a period */ export function identifyPolicyRegimes(actions: FedPolicyAction[]): PolicyRegime[] { if (!actions || actions.length === 0) return []; const regimes: PolicyRegime[] = []; let currentRegime: PolicyRegime | null = null; let regimeActions: FedPolicyAction[] = []; for (const action of actions) { const isEasing = action.action === 'CUT'; const isTightening = action.action === 'HIKE'; if (!currentRegime) { // Start new regime currentRegime = { type: isEasing ? 'easing' : isTightening ? 'tightening' : 'neutral', startDate: action.date, endDate: action.date, totalChange: action.changeBps || 0, duration: 1, context: '' }; regimeActions = [action]; } else { // Check if regime continues or changes const sameDirection = (currentRegime.type === 'easing' && isEasing) || (currentRegime.type === 'tightening' && isTightening) || (currentRegime.type === 'neutral' && action.action === 'HOLD'); if (sameDirection || Math.abs(action.changeBps || 0) < 25) { // Continue current regime currentRegime.endDate = action.date; currentRegime.totalChange += action.changeBps || 0; currentRegime.duration++; regimeActions.push(action); } else { // End current regime and start new one currentRegime.context = generateRegimeContext(currentRegime, regimeActions); regimes.push(currentRegime); currentRegime = { type: isEasing ? 'easing' : isTightening ? 'tightening' : 'neutral', startDate: action.date, endDate: action.date, totalChange: action.changeBps || 0, duration: 1, context: '' }; regimeActions = [action]; } } } // Add final regime if (currentRegime) { currentRegime.context = generateRegimeContext(currentRegime, regimeActions); regimes.push(currentRegime); } return regimes; } /** * Generate context description for a policy regime */ function generateRegimeContext(regime: PolicyRegime, actions: FedPolicyAction[]): string { const avgChange = regime.totalChange / regime.duration; if (regime.type === 'easing') { if (regime.totalChange < -200) { return 'Emergency easing response'; } else if (regime.totalChange < -100) { return 'Aggressive easing cycle'; } else { return 'Gradual accommodation'; } } else if (regime.type === 'tightening') { if (regime.totalChange > 200) { return 'Aggressive inflation fight'; } else if (regime.totalChange > 100) { return 'Steady tightening cycle'; } else { return 'Cautious rate normalization'; } } return 'Wait-and-see approach'; } /** * Project future Fed actions based on historical patterns */ export function projectFuturePolicy( currentData: EconomicDataPoint[], historicalAnalogue: HistoricalAnalogue ): PolicyProjection[] { const projections: PolicyProjection[] = []; // Analyze the pattern from the historical analogue const regimes = identifyPolicyRegimes(historicalAnalogue.fedPolicyActions); if (regimes.length === 0) return projections; // Get current economic conditions const latestData = currentData[currentData.length - 1]; const inflation = latestData.CPIAUCSL as number || 0; const unemployment = latestData.UNRATE as number || 0; const gdp = latestData.GDPC1 as number || 0; // Project next 6 months based on historical pattern const monthsAhead = 6; const currentDate = new Date(); for (let i = 1; i <= monthsAhead; i++) { const projectedDate = new Date(currentDate); projectedDate.setMonth(projectedDate.getMonth() + i); const monthStr = projectedDate.toISOString().substring(0, 7); // Determine likely action based on conditions and historical pattern let action: PolicyProjection['action'] = 'HOLD'; let probability = 50; let basisPoints = 0; let rationale = ''; if (inflation > 3 && unemployment < 4.5) { // Inflationary pressure scenario action = 'HIKE'; probability = 70; basisPoints = 25; rationale = 'Inflation above target with tight labor market'; } else if (unemployment > 4.5 && inflation < 2) { // Recessionary pressure scenario action = 'CUT'; probability = 75; basisPoints = -25; rationale = 'Rising unemployment with low inflation'; } else if (gdp < 1) { // Growth concern scenario action = 'CUT'; probability = 60; basisPoints = -25; rationale = 'Weak economic growth'; } // Adjust based on historical pattern const dominantRegime = regimes[0]; if (dominantRegime.type === 'easing' && action !== 'HIKE') { probability += 10; } else if (dominantRegime.type === 'tightening' && action !== 'CUT') { probability += 10; } projections.push({ month: monthStr, action, probability, basisPoints, rationale }); } return projections; } /** * Generate a policy playbook based on historical patterns */ export function generatePolicyPlaybook(analogue: HistoricalAnalogue): PolicyPlaybook { const regimes = identifyPolicyRegimes(analogue.fedPolicyActions); const actions = analogue.fedPolicyActions.filter(a => a.action !== 'HOLD'); // Identify key triggers from the data const keyTriggers: string[] = []; const data = analogue.data; if (data.length > 0) { const avgInflation = data.reduce((sum, d) => sum + (d.CPIAUCSL as number || 0), 0) / data.length; const avgUnemployment = data.reduce((sum, d) => sum + (d.UNRATE as number || 0), 0) / data.length; if (avgInflation > 3) keyTriggers.push('Elevated inflation (>3%)'); if (avgUnemployment > 5) keyTriggers.push('Rising unemployment (>5%)'); if (avgInflation < 2) keyTriggers.push('Below-target inflation (<2%)'); } // Build typical sequence const typicalSequence = regimes.map(r => { const change = r.totalChange > 0 ? `+${r.totalChange}` : `${r.totalChange}`; return `${r.context} (${change}bps over ${r.duration} months)`; }); // Calculate success metrics const totalDuration = regimes.reduce((sum, r) => sum + r.duration, 0); const historicalSuccess = calculatePolicySuccess(analogue); return { pattern: regimes[0]?.context || 'No clear pattern', keyTriggers, typicalSequence, expectedDuration: totalDuration, historicalSuccess }; } /** * Calculate how successful the Fed policy was in achieving objectives */ function calculatePolicySuccess(analogue: HistoricalAnalogue): number { const data = analogue.data; if (data.length < 6) return 50; // Not enough data // Compare start vs end conditions const startInflation = data[0].CPIAUCSL as number || 0; const endInflation = data[data.length - 1].CPIAUCSL as number || 0; const startUnemployment = data[0].UNRATE as number || 0; const endUnemployment = data[data.length - 1].UNRATE as number || 0; let score = 50; // Base score // Inflation control if (startInflation > 3 && endInflation < startInflation) { score += 20; // Successfully reduced high inflation } else if (startInflation < 2 && endInflation > startInflation && endInflation < 3) { score += 15; // Successfully raised low inflation toward target } // Employment if (startUnemployment > 5 && endUnemployment < startUnemployment) { score += 20; // Successfully reduced unemployment } else if (startUnemployment < 4 && endUnemployment < 5) { score += 10; // Maintained low unemployment } // Stability const inflationVolatility = Math.abs(endInflation - startInflation); if (inflationVolatility < 1) { score += 15; // Maintained price stability } return Math.min(100, Math.max(0, score)); } /** * Generate forward guidance projections based on historical patterns */ export function generateForwardGuidance(analogue: HistoricalAnalogue): PolicyProjection[] { const projections: PolicyProjection[] = []; const actions = analogue.fedPolicyActions || []; const data = analogue.data; if (data.length === 0) return projections; // Get latest economic conditions const latestData = data[data.length - 1]; const inflation = latestData.CPIAUCSL as number || 2.5; const unemployment = latestData.UNRATE as number || 4.0; const gdpGrowth = latestData.GDPC1 as number || 2.0; // Identify current regime const regimes = identifyPolicyRegimes(actions); const currentRegime = regimes[regimes.length - 1]; // Project next 6 months based on historical patterns and current conditions const months = ['Month 1', 'Month 2', 'Month 3', 'Month 4', 'Month 5', 'Month 6']; for (let i = 0; i < months.length; i++) { let action: PolicyProjection['action'] = 'HOLD'; let probability = 50; let basisPoints = 0; let rationale = 'Data-dependent approach'; // Determine likely action based on conditions if (inflation > 3.5 && unemployment < 4.5) { // Inflationary pressure if (i < 2) { action = 'HIKE'; basisPoints = 25; probability = 60 + (inflation - 3.5) * 20; rationale = 'Persistent inflation requires continued tightening'; } else { action = 'HOLD'; probability = 70; rationale = 'Assessing cumulative impact of rate hikes'; } } else if (unemployment > 4.5 && inflation < 3.0) { // Economic weakness if (i < 3) { action = 'CUT'; basisPoints = 25; probability = 55 + (unemployment - 4.5) * 20; rationale = 'Labor market softening warrants easing'; } else { action = 'HOLD'; probability = 60; rationale = 'Monitoring policy transmission'; } } else if (gdpGrowth < 1.0) { // Growth concerns action = 'CUT'; basisPoints = 25; probability = 65; rationale = 'Preemptive easing to support growth'; } else { // Balanced conditions action = 'HOLD'; probability = 75; rationale = 'Economic conditions near equilibrium'; } // Adjust based on historical pattern if (currentRegime && i < 3) { if (currentRegime.type === 'easing' && action !== 'HIKE') { probability += 10; rationale += '; continuing easing cycle'; } else if (currentRegime.type === 'tightening' && action !== 'CUT') { probability += 10; rationale += '; maintaining restrictive stance'; } } projections.push({ month: months[i], action, probability: Math.min(90, Math.max(10, probability)), basisPoints: action === 'HOLD' ? undefined : basisPoints, rationale }); } return projections; }