"""Macro Analyst Agent. Specializes in macroeconomic analysis using FRED data: - Economic regime detection (expansion, contraction, stagflation) - Interest rate environment analysis - Yield curve interpretation - Money supply and liquidity analysis - Inflation regime classification - GDP growth assessment Issue #14: [AGENT-13] Macro Analyst - FRED interpretation, regime detection """ from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder from langchain_core.tools import tool from typing import Annotated, Dict, Any, List, Optional import pandas as pd from enum import Enum # ============================================================================ # Economic Regime Definitions # ============================================================================ class EconomicRegime(str, Enum): """Economic regime classifications.""" EXPANSION = "expansion" LATE_CYCLE = "late_cycle" CONTRACTION = "contraction" EARLY_RECOVERY = "early_recovery" STAGFLATION = "stagflation" GOLDILOCKS = "goldilocks" # Low inflation, moderate growth class YieldCurveState(str, Enum): """Yield curve state classifications.""" NORMAL = "normal" # 2Y < 10Y (positive slope) FLAT = "flat" # 2Y ≈ 10Y (within 25bp) INVERTED = "inverted" # 2Y > 10Y (negative slope) STEEP = "steep" # Large positive spread (>200bp) class MonetaryPolicy(str, Enum): """Monetary policy stance classifications.""" HAWKISH = "hawkish" # Rising rates, fighting inflation NEUTRAL = "neutral" # Stable rates DOVISH = "dovish" # Falling rates, supporting growth EMERGENCY = "emergency" # Near-zero rates class InflationRegime(str, Enum): """Inflation regime classifications.""" DEFLATION = "deflation" # < 0% LOW = "low" # 0-2% TARGET = "target" # 2-3% ELEVATED = "elevated" # 3-5% HIGH = "high" # > 5% # ============================================================================ # FRED Data Access Helpers # ============================================================================ def _get_fred_data(series_id: str, start_date: str = None, end_date: str = None) -> pd.DataFrame: """Helper to get FRED data with proper error handling.""" try: from tradingagents.dataflows.fred import ( get_interest_rates, get_treasury_rates, get_money_supply, get_gdp, get_inflation, get_unemployment, get_fred_series, ) # Route to appropriate function based on series series_mapping = { 'FEDFUNDS': lambda: get_interest_rates(start_date=start_date, end_date=end_date), 'DGS2': lambda: get_treasury_rates('2Y', start_date=start_date, end_date=end_date), 'DGS10': lambda: get_treasury_rates('10Y', start_date=start_date, end_date=end_date), 'M2SL': lambda: get_money_supply('M2', start_date=start_date, end_date=end_date), 'GDP': lambda: get_gdp(start_date=start_date, end_date=end_date), 'CPIAUCSL': lambda: get_inflation('CPI', start_date=start_date, end_date=end_date), 'UNRATE': lambda: get_unemployment(start_date=start_date, end_date=end_date), } if series_id in series_mapping: return series_mapping[series_id]() else: return get_fred_series(series_id, start_date=start_date, end_date=end_date) except ImportError: # Fallback for testing without full FRED module return pd.DataFrame() except Exception as e: return pd.DataFrame() # ============================================================================ # Macro Analysis Tools # ============================================================================ @tool def get_economic_regime_analysis( curr_date: Annotated[str, "Current analysis date in YYYY-MM-DD format"], look_back_months: Annotated[int, "Months of history to analyze (default: 12)"] = 12, ) -> str: """ Analyze current economic regime using multiple FRED indicators. Considers: - GDP growth (expansion vs contraction) - Unemployment trend (improving vs deteriorating) - Inflation level (target vs elevated) - Interest rate direction Returns comprehensive regime classification with supporting data. """ try: from datetime import datetime, timedelta # Calculate date range end_date = curr_date start_dt = datetime.strptime(curr_date, "%Y-%m-%d") - timedelta(days=look_back_months * 30) start_date = start_dt.strftime("%Y-%m-%d") # Collect economic indicators indicators = {} # Get GDP data gdp_data = _get_fred_data('GDP', start_date=start_date, end_date=end_date) if isinstance(gdp_data, pd.DataFrame) and not gdp_data.empty: gdp_growth = _calculate_growth_rate(gdp_data) indicators['gdp_growth'] = gdp_growth # Get unemployment data unemp_data = _get_fred_data('UNRATE', start_date=start_date, end_date=end_date) if isinstance(unemp_data, pd.DataFrame) and not unemp_data.empty: unemp_level = unemp_data['value'].iloc[-1] if 'value' in unemp_data.columns else None unemp_trend = _calculate_trend(unemp_data) indicators['unemployment'] = unemp_level indicators['unemployment_trend'] = unemp_trend # Get inflation data cpi_data = _get_fred_data('CPIAUCSL', start_date=start_date, end_date=end_date) if isinstance(cpi_data, pd.DataFrame) and not cpi_data.empty: inflation_rate = _calculate_yoy_change(cpi_data) indicators['inflation'] = inflation_rate # Get Fed Funds Rate ffr_data = _get_fred_data('FEDFUNDS', start_date=start_date, end_date=end_date) if isinstance(ffr_data, pd.DataFrame) and not ffr_data.empty: fed_rate = ffr_data['value'].iloc[-1] if 'value' in ffr_data.columns else None fed_trend = _calculate_trend(ffr_data) indicators['fed_funds_rate'] = fed_rate indicators['fed_trend'] = fed_trend # Determine economic regime regime = _classify_economic_regime(indicators) # Generate report report = f""" ## Economic Regime Analysis Analysis Date: {curr_date} Look-back Period: {look_back_months} months ### Current Regime: {regime.value.upper()} ### Key Economic Indicators | Indicator | Current Value | Trend | Signal | |-----------|---------------|-------|--------| | GDP Growth | {indicators.get('gdp_growth', 'N/A'):.1f}% | {_trend_to_arrow(indicators.get('gdp_growth', 0))} | {_gdp_signal(indicators.get('gdp_growth'))} | | Unemployment | {indicators.get('unemployment', 'N/A'):.1f}% | {_trend_to_arrow(-indicators.get('unemployment_trend', 0))} | {_unemployment_signal(indicators.get('unemployment'))} | | Inflation (YoY) | {indicators.get('inflation', 'N/A'):.1f}% | {_trend_to_arrow(indicators.get('inflation', 0) - 2)} | {_inflation_signal(indicators.get('inflation'))} | | Fed Funds Rate | {indicators.get('fed_funds_rate', 'N/A'):.2f}% | {_trend_to_arrow(indicators.get('fed_trend', 0))} | {_policy_signal(indicators.get('fed_trend'))} | ### Regime Interpretation {_regime_interpretation(regime)} ### Investment Implications {_regime_investment_implications(regime)} """ return report except Exception as e: return f"Error in economic regime analysis: {str(e)}" @tool def get_yield_curve_analysis( curr_date: Annotated[str, "Current analysis date in YYYY-MM-DD format"], look_back_days: Annotated[int, "Days of history to analyze (default: 252)"] = 252, ) -> str: """ Analyze yield curve shape and implications. Examines: - 2Y-10Y spread (primary recession indicator) - 3M-10Y spread (Fed's preferred measure) - Historical context and duration of current state - Recession probability based on inversion history Returns yield curve analysis with recession probability. """ try: from datetime import datetime, timedelta end_date = curr_date start_dt = datetime.strptime(curr_date, "%Y-%m-%d") - timedelta(days=look_back_days) start_date = start_dt.strftime("%Y-%m-%d") # Get yield data dgs2_data = _get_fred_data('DGS2', start_date=start_date, end_date=end_date) dgs10_data = _get_fred_data('DGS10', start_date=start_date, end_date=end_date) # Calculate current spread current_2y = None current_10y = None current_spread = None if isinstance(dgs2_data, pd.DataFrame) and not dgs2_data.empty: current_2y = dgs2_data['value'].iloc[-1] if 'value' in dgs2_data.columns else dgs2_data.iloc[-1, 0] if isinstance(dgs10_data, pd.DataFrame) and not dgs10_data.empty: current_10y = dgs10_data['value'].iloc[-1] if 'value' in dgs10_data.columns else dgs10_data.iloc[-1, 0] if current_2y is not None and current_10y is not None: current_spread = current_10y - current_2y # Determine yield curve state curve_state = _classify_yield_curve(current_spread) # Calculate inversion metrics inversion_days = 0 avg_spread = None if isinstance(dgs2_data, pd.DataFrame) and isinstance(dgs10_data, pd.DataFrame): spread_series = _calculate_spread_series(dgs2_data, dgs10_data) if len(spread_series) > 0: inversion_days = len([s for s in spread_series if s < 0]) avg_spread = sum(spread_series) / len(spread_series) # Recession probability based on inversion recession_prob = _calculate_recession_probability(curve_state, inversion_days, look_back_days) report = f""" ## Yield Curve Analysis Analysis Date: {curr_date} Look-back Period: {look_back_days} days ### Current Yield Curve State: {curve_state.value.upper()} ### Treasury Yields | Maturity | Current Yield | |----------|---------------| | 2-Year | {current_2y:.2f}% | | 10-Year | {current_10y:.2f}% | | **Spread (10Y-2Y)** | **{current_spread:.0f} bp** | ### Curve Metrics - **Current Spread**: {current_spread:.0f} basis points - **Average Spread (Period)**: {avg_spread:.0f if avg_spread else 'N/A'} bp - **Days Inverted**: {inversion_days} of {look_back_days} days ({inversion_days/look_back_days*100:.1f}%) ### Recession Probability Based on yield curve analysis: **{recession_prob:.0f}%** {_yield_curve_interpretation(curve_state, recession_prob)} ### Historical Context {_yield_curve_historical_context(curve_state, inversion_days)} """ return report except Exception as e: return f"Error in yield curve analysis: {str(e)}" @tool def get_monetary_policy_analysis( curr_date: Annotated[str, "Current analysis date in YYYY-MM-DD format"], look_back_months: Annotated[int, "Months of history to analyze (default: 24)"] = 24, ) -> str: """ Analyze Federal Reserve monetary policy stance and direction. Examines: - Federal Funds Rate level and trajectory - Real interest rates (Fed Funds - Inflation) - Money supply (M2) growth - Policy stance classification Returns monetary policy analysis with stance assessment. """ try: from datetime import datetime, timedelta end_date = curr_date start_dt = datetime.strptime(curr_date, "%Y-%m-%d") - timedelta(days=look_back_months * 30) start_date = start_dt.strftime("%Y-%m-%d") # Get Fed Funds Rate ffr_data = _get_fred_data('FEDFUNDS', start_date=start_date, end_date=end_date) current_ffr = None ffr_change_6m = None ffr_change_12m = None if isinstance(ffr_data, pd.DataFrame) and not ffr_data.empty: values = ffr_data['value'] if 'value' in ffr_data.columns else ffr_data.iloc[:, 0] current_ffr = values.iloc[-1] if len(values) > 126: # ~6 months of daily data ffr_change_6m = current_ffr - values.iloc[-126] if len(values) > 252: # ~12 months ffr_change_12m = current_ffr - values.iloc[-252] # Get M2 money supply m2_data = _get_fred_data('M2SL', start_date=start_date, end_date=end_date) m2_growth = None if isinstance(m2_data, pd.DataFrame) and not m2_data.empty: m2_growth = _calculate_yoy_change(m2_data) # Get inflation cpi_data = _get_fred_data('CPIAUCSL', start_date=start_date, end_date=end_date) inflation = None if isinstance(cpi_data, pd.DataFrame) and not cpi_data.empty: inflation = _calculate_yoy_change(cpi_data) # Calculate real rate real_rate = None if current_ffr is not None and inflation is not None: real_rate = current_ffr - inflation # Determine policy stance policy_stance = _classify_monetary_policy(current_ffr, ffr_change_6m, inflation) report = f""" ## Monetary Policy Analysis Analysis Date: {curr_date} Look-back Period: {look_back_months} months ### Policy Stance: {policy_stance.value.upper()} ### Federal Funds Rate | Metric | Value | |--------|-------| | Current Rate | {current_ffr:.2f}% | | 6-Month Change | {'+' if ffr_change_6m and ffr_change_6m > 0 else ''}{ffr_change_6m:.2f if ffr_change_6m else 'N/A'}% | | 12-Month Change | {'+' if ffr_change_12m and ffr_change_12m > 0 else ''}{ffr_change_12m:.2f if ffr_change_12m else 'N/A'}% | ### Real Interest Rate - **Nominal Rate (FFR)**: {current_ffr:.2f}% - **Inflation Rate**: {inflation:.2f if inflation else 'N/A'}% - **Real Rate**: {real_rate:.2f if real_rate else 'N/A'}% {_real_rate_interpretation(real_rate)} ### Liquidity Conditions | Metric | Value | Signal | |--------|-------|--------| | M2 Growth (YoY) | {m2_growth:.1f if m2_growth else 'N/A'}% | {_m2_signal(m2_growth)} | ### Policy Direction Assessment {_policy_direction_interpretation(policy_stance, ffr_change_6m)} ### Market Implications {_policy_market_implications(policy_stance, real_rate)} """ return report except Exception as e: return f"Error in monetary policy analysis: {str(e)}" @tool def get_inflation_regime_analysis( curr_date: Annotated[str, "Current analysis date in YYYY-MM-DD format"], look_back_months: Annotated[int, "Months of history to analyze (default: 36)"] = 36, ) -> str: """ Analyze inflation regime and trajectory. Examines: - CPI headline and core - PCE (Fed's preferred measure) - Inflation trend (accelerating/decelerating) - Inflation expectations Returns inflation regime analysis with investment implications. """ try: from datetime import datetime, timedelta end_date = curr_date start_dt = datetime.strptime(curr_date, "%Y-%m-%d") - timedelta(days=look_back_months * 30) start_date = start_dt.strftime("%Y-%m-%d") # Get CPI data cpi_data = _get_fred_data('CPIAUCSL', start_date=start_date, end_date=end_date) cpi_yoy = None cpi_3m_annualized = None cpi_trend = None if isinstance(cpi_data, pd.DataFrame) and not cpi_data.empty: cpi_yoy = _calculate_yoy_change(cpi_data) cpi_3m_annualized = _calculate_annualized_3m_change(cpi_data) cpi_trend = "accelerating" if cpi_3m_annualized and cpi_yoy and cpi_3m_annualized > cpi_yoy else "decelerating" # Determine inflation regime inflation_regime = _classify_inflation_regime(cpi_yoy) # Calculate deviation from target target_deviation = (cpi_yoy - 2.0) if cpi_yoy else None report = f""" ## Inflation Regime Analysis Analysis Date: {curr_date} Look-back Period: {look_back_months} months ### Current Regime: {inflation_regime.value.upper()} ### Inflation Metrics | Measure | Value | Target Deviation | |---------|-------|------------------| | CPI (YoY) | {cpi_yoy:.1f if cpi_yoy else 'N/A'}% | {'+' if target_deviation and target_deviation > 0 else ''}{target_deviation:.1f if target_deviation else 'N/A'}% | | CPI (3M Annualized) | {cpi_3m_annualized:.1f if cpi_3m_annualized else 'N/A'}% | {_momentum_signal(cpi_3m_annualized, cpi_yoy)} | ### Inflation Trajectory: {cpi_trend.upper() if cpi_trend else 'UNKNOWN'} {_inflation_trajectory_interpretation(cpi_trend, cpi_yoy, cpi_3m_annualized)} ### Regime Implications {_inflation_regime_interpretation(inflation_regime)} ### Asset Class Impact {_inflation_asset_impact(inflation_regime)} """ return report except Exception as e: return f"Error in inflation regime analysis: {str(e)}" # ============================================================================ # Helper Functions # ============================================================================ def _calculate_growth_rate(data: pd.DataFrame) -> float: """Calculate annualized growth rate from data.""" if data.empty or len(data) < 4: return 0.0 values = data['value'] if 'value' in data.columns else data.iloc[:, 0] if len(values) >= 4: # Quarterly data: compare to 4 quarters ago return ((values.iloc[-1] / values.iloc[-4]) - 1) * 100 return 0.0 def _calculate_trend(data: pd.DataFrame) -> float: """Calculate trend direction (-1 to 1).""" if data.empty or len(data) < 2: return 0.0 values = data['value'] if 'value' in data.columns else data.iloc[:, 0] if len(values) >= 10: recent = values.iloc[-5:].mean() earlier = values.iloc[-10:-5].mean() if earlier != 0: return (recent - earlier) / abs(earlier) return 0.0 def _calculate_yoy_change(data: pd.DataFrame) -> float: """Calculate year-over-year percentage change.""" if data.empty or len(data) < 12: return 0.0 values = data['value'] if 'value' in data.columns else data.iloc[:, 0] if len(values) >= 12: return ((values.iloc[-1] / values.iloc[-12]) - 1) * 100 return 0.0 def _calculate_annualized_3m_change(data: pd.DataFrame) -> float: """Calculate 3-month change annualized.""" if data.empty or len(data) < 3: return 0.0 values = data['value'] if 'value' in data.columns else data.iloc[:, 0] if len(values) >= 3: return ((values.iloc[-1] / values.iloc[-3]) ** 4 - 1) * 100 return 0.0 def _calculate_spread_series(data_2y: pd.DataFrame, data_10y: pd.DataFrame) -> List[float]: """Calculate spread series between two yield series.""" try: v2y = data_2y['value'] if 'value' in data_2y.columns else data_2y.iloc[:, 0] v10y = data_10y['value'] if 'value' in data_10y.columns else data_10y.iloc[:, 0] min_len = min(len(v2y), len(v10y)) return [(v10y.iloc[i] - v2y.iloc[i]) * 100 for i in range(min_len)] # Convert to bp except Exception: return [] def _classify_economic_regime(indicators: Dict) -> EconomicRegime: """Classify economic regime based on indicators.""" gdp = indicators.get('gdp_growth', 0) inflation = indicators.get('inflation', 2) unemployment = indicators.get('unemployment', 5) if gdp > 2 and inflation < 3 and unemployment < 5: return EconomicRegime.GOLDILOCKS elif gdp < 0: return EconomicRegime.CONTRACTION elif gdp < 0 and inflation > 4: return EconomicRegime.STAGFLATION elif gdp > 3: return EconomicRegime.EXPANSION elif indicators.get('unemployment_trend', 0) > 0: return EconomicRegime.LATE_CYCLE else: return EconomicRegime.EARLY_RECOVERY def _classify_yield_curve(spread: float) -> YieldCurveState: """Classify yield curve state based on 2Y-10Y spread.""" if spread is None: return YieldCurveState.NORMAL if spread < -25: return YieldCurveState.INVERTED elif spread < 25: return YieldCurveState.FLAT elif spread > 200: return YieldCurveState.STEEP else: return YieldCurveState.NORMAL def _classify_monetary_policy(rate: float, change_6m: float, inflation: float) -> MonetaryPolicy: """Classify monetary policy stance.""" if rate is None: return MonetaryPolicy.NEUTRAL if rate < 0.5: return MonetaryPolicy.EMERGENCY elif change_6m is not None and change_6m > 0.5: return MonetaryPolicy.HAWKISH elif change_6m is not None and change_6m < -0.5: return MonetaryPolicy.DOVISH else: return MonetaryPolicy.NEUTRAL def _classify_inflation_regime(inflation: float) -> InflationRegime: """Classify inflation regime based on rate.""" if inflation is None: return InflationRegime.TARGET if inflation < 0: return InflationRegime.DEFLATION elif inflation < 2: return InflationRegime.LOW elif inflation < 3: return InflationRegime.TARGET elif inflation < 5: return InflationRegime.ELEVATED else: return InflationRegime.HIGH def _calculate_recession_probability(state: YieldCurveState, inversion_days: int, total_days: int) -> float: """Calculate recession probability based on yield curve.""" base_prob = 0 if state == YieldCurveState.INVERTED: base_prob = 50 elif state == YieldCurveState.FLAT: base_prob = 25 # Adjust based on inversion duration if total_days > 0: inversion_ratio = inversion_days / total_days if inversion_ratio > 0.5: base_prob = min(base_prob + 25, 80) elif inversion_ratio > 0.25: base_prob = min(base_prob + 15, 70) return base_prob def _trend_to_arrow(value: float) -> str: """Convert trend value to arrow indicator.""" if value is None: return "➡️" if value > 0.1: return "⬆️" elif value < -0.1: return "⬇️" else: return "➡️" def _gdp_signal(growth: float) -> str: """Generate signal based on GDP growth.""" if growth is None: return "N/A" if growth > 3: return "🟢 Strong" elif growth > 1: return "🟡 Moderate" elif growth > 0: return "🟠 Slow" else: return "🔴 Contraction" def _unemployment_signal(rate: float) -> str: """Generate signal based on unemployment.""" if rate is None: return "N/A" if rate < 4: return "🟢 Tight Labor" elif rate < 5: return "🟢 Healthy" elif rate < 6: return "🟡 Softening" else: return "🔴 Elevated" def _inflation_signal(rate: float) -> str: """Generate signal based on inflation.""" if rate is None: return "N/A" if rate < 2: return "🟢 Below Target" elif rate < 3: return "🟢 At Target" elif rate < 4: return "🟡 Elevated" else: return "🔴 High" def _policy_signal(trend: float) -> str: """Generate signal based on policy trend.""" if trend is None: return "➡️ Stable" if trend > 0.1: return "⬆️ Tightening" elif trend < -0.1: return "⬇️ Easing" else: return "➡️ Stable" def _m2_signal(growth: float) -> str: """Generate signal based on M2 growth.""" if growth is None: return "N/A" if growth > 10: return "🟢 Expanding" elif growth > 5: return "🟢 Moderate" elif growth > 0: return "🟡 Slow" else: return "🔴 Contracting" def _momentum_signal(short_term: float, long_term: float) -> str: """Compare short vs long term for momentum.""" if short_term is None or long_term is None: return "N/A" if short_term > long_term + 0.5: return "⬆️ Accelerating" elif short_term < long_term - 0.5: return "⬇️ Decelerating" else: return "➡️ Stable" def _regime_interpretation(regime: EconomicRegime) -> str: """Generate interpretation text for economic regime.""" interpretations = { EconomicRegime.EXPANSION: "The economy is in a healthy expansion phase with robust growth, moderate inflation, and improving employment. This environment typically favors risk assets.", EconomicRegime.LATE_CYCLE: "Signs of late-cycle dynamics are emerging. Growth may be peaking while labor markets are tight. Watch for rising inflation and yield curve flattening.", EconomicRegime.CONTRACTION: "The economy is contracting. GDP is declining and unemployment may be rising. Defensive positioning and quality focus recommended.", EconomicRegime.EARLY_RECOVERY: "Early signs of economic recovery are appearing. Growth is returning but remains fragile. Early-cycle sectors may outperform.", EconomicRegime.STAGFLATION: "Stagflation conditions present: weak growth combined with elevated inflation. A challenging environment for most asset classes.", EconomicRegime.GOLDILOCKS: "A 'Goldilocks' scenario with moderate growth, low inflation, and healthy employment. Generally positive for risk assets.", } return interpretations.get(regime, "Economic conditions are mixed.") def _regime_investment_implications(regime: EconomicRegime) -> str: """Generate investment implications for economic regime.""" implications = { EconomicRegime.EXPANSION: """- **Equities**: Overweight cyclical sectors (Industrials, Financials, Materials) - **Fixed Income**: Underweight duration, favor credit - **Commodities**: Constructive on industrial metals - **Real Estate**: Favor economically-sensitive REITs""", EconomicRegime.LATE_CYCLE: """- **Equities**: Shift toward quality and defensive sectors - **Fixed Income**: Begin adding duration, reduce credit risk - **Commodities**: Mixed outlook, monitor demand signals - **Cash**: Increase allocation as hedge""", EconomicRegime.CONTRACTION: """- **Equities**: Defensive sectors (Utilities, Healthcare, Consumer Staples) - **Fixed Income**: Overweight Treasuries, extend duration - **Commodities**: Underweight cyclical commodities - **Cash**: Elevated allocation appropriate""", EconomicRegime.EARLY_RECOVERY: """- **Equities**: Favor small caps and value stocks - **Fixed Income**: Reduce duration as recovery strengthens - **Commodities**: Early-cycle commodities may rally - **Real Estate**: Recovery in cyclical REITs""", EconomicRegime.STAGFLATION: """- **Equities**: Quality dividend payers, inflation hedges - **Fixed Income**: TIPS, short duration - **Commodities**: Gold and commodity producers - **Real Assets**: Inflation-linked real assets""", EconomicRegime.GOLDILOCKS: """- **Equities**: Broad market exposure, growth stocks - **Fixed Income**: Modest duration, credit exposure - **Commodities**: Neutral to constructive - **Alternative**: Risk-on positioning appropriate""", } return implications.get(regime, "Maintain balanced allocation.") def _yield_curve_interpretation(state: YieldCurveState, recession_prob: float) -> str: """Generate interpretation for yield curve state.""" if state == YieldCurveState.INVERTED: return f"""⚠️ **Inverted Yield Curve Warning** The yield curve is inverted (2Y yield exceeds 10Y), historically a reliable recession predictor. Since 1955, an inverted yield curve has preceded every recession with an average lead time of 12-18 months. Current recession probability: {recession_prob:.0f}% Note: The yield curve can remain inverted for extended periods before recession materializes.""" elif state == YieldCurveState.FLAT: return f"""📊 **Flattening Yield Curve** The yield curve is flat, indicating uncertainty about future growth and monetary policy direction. This often precedes either inversion (bearish) or steepening (bullish). Monitor for: Further flattening toward inversion, or steepening on policy pivot.""" elif state == YieldCurveState.STEEP: return f"""📈 **Steep Yield Curve** The steep yield curve suggests expectations of accelerating growth and/or rising inflation. This is typically seen in early recovery phases and is generally positive for banks and cyclical sectors.""" else: return f"""✅ **Normal Yield Curve** The yield curve has a normal positive slope, suggesting healthy expectations for growth without imminent recession concerns. This is a constructive backdrop for risk assets.""" def _yield_curve_historical_context(state: YieldCurveState, inversion_days: int) -> str: """Generate historical context for yield curve.""" if state == YieldCurveState.INVERTED: return """**Historical Inversions and Recessions:** - 2019-2020: Inverted → COVID recession (2020) - 2006-2007: Inverted → Financial Crisis (2008) - 2000-2001: Inverted → Dot-com recession (2001) - 1989-1990: Inverted → 1990 recession Average lead time: 12-18 months from first inversion.""" return "The yield curve's current shape is consistent with historical patterns during similar economic conditions." def _real_rate_interpretation(real_rate: float) -> str: """Interpret real interest rate level.""" if real_rate is None: return "" if real_rate > 2: return "**Restrictive**: Real rates are significantly positive, indicating tight monetary conditions that may slow economic growth." elif real_rate > 0: return "**Neutral to Tight**: Positive real rates suggest monetary policy is not accommodative but not severely restrictive." elif real_rate > -2: return "**Accommodative**: Negative real rates indicate easy monetary conditions that support growth and asset prices." else: return "**Highly Accommodative**: Deeply negative real rates represent emergency monetary accommodation." def _policy_direction_interpretation(stance: MonetaryPolicy, change: float) -> str: """Interpret monetary policy direction.""" if stance == MonetaryPolicy.HAWKISH: return "The Fed is in tightening mode, raising rates to combat inflation. This typically creates headwinds for rate-sensitive assets and may slow economic growth." elif stance == MonetaryPolicy.DOVISH: return "The Fed is easing monetary policy, cutting rates to support growth. This is generally supportive for risk assets and rate-sensitive sectors." elif stance == MonetaryPolicy.EMERGENCY: return "Emergency monetary conditions with rates near zero. The Fed is providing maximum accommodation to support the economy." else: return "Monetary policy is in a neutral stance with rates stable. Watch for signals of future direction changes." def _policy_market_implications(stance: MonetaryPolicy, real_rate: float) -> str: """Generate market implications for monetary policy.""" if stance == MonetaryPolicy.HAWKISH: return """- **Equities**: Headwind for growth stocks, favor value - **Fixed Income**: Duration risk, favor short-term - **USD**: Supportive for dollar strength - **Gold**: Headwind from rising real rates""" elif stance == MonetaryPolicy.DOVISH: return """- **Equities**: Supportive for growth stocks - **Fixed Income**: Rally potential in longer duration - **USD**: Potential dollar weakness - **Gold**: Supportive from falling real rates""" elif stance == MonetaryPolicy.EMERGENCY: return """- **Equities**: Maximum policy support, but monitor fundamentals - **Fixed Income**: Very low yields, consider credit for income - **USD**: Potential weakness from accommodation - **Gold**: Historically supportive environment""" else: return """- **Equities**: Monitor for policy pivot signals - **Fixed Income**: Neutral positioning appropriate - **USD**: Data-dependent direction - **Gold**: Balanced outlook""" def _inflation_trajectory_interpretation(trend: str, yoy: float, short_term: float) -> str: """Interpret inflation trajectory.""" if trend == "accelerating": return f"Inflation momentum is **accelerating**, with the 3-month annualized rate ({short_term:.1f}%) exceeding the year-over-year rate ({yoy:.1f}%). This suggests upward pressure on prices and potential Fed response." elif trend == "decelerating": return f"Inflation is **decelerating**, with the 3-month annualized rate ({short_term:.1f}%) below the year-over-year rate ({yoy:.1f}%). This suggests easing price pressures, potentially allowing for more accommodative policy." return "Inflation momentum is relatively stable." def _inflation_regime_interpretation(regime: InflationRegime) -> str: """Interpret inflation regime implications.""" interpretations = { InflationRegime.DEFLATION: "Deflationary conditions are rare and concerning, typically associated with economic distress. Central banks will aggressively fight deflation.", InflationRegime.LOW: "Low inflation below the 2% target may prompt continued monetary accommodation. Watch for disinflation risks.", InflationRegime.TARGET: "Inflation is at or near the Fed's 2% target - the 'sweet spot' for monetary policy. This allows for balanced policy decisions.", InflationRegime.ELEVATED: "Elevated inflation above target will keep the Fed focused on price stability. Expect tighter monetary conditions until inflation returns to target.", InflationRegime.HIGH: "High inflation is the primary policy concern. Aggressive monetary tightening is likely until inflation shows sustained decline.", } return interpretations.get(regime, "") def _inflation_asset_impact(regime: InflationRegime) -> str: """Generate asset class impact for inflation regime.""" impacts = { InflationRegime.DEFLATION: """| Asset Class | Impact | Recommendation | |-------------|--------|----------------| | Equities | Negative | Defensive, quality focus | | Bonds | Positive | Long duration, Treasuries | | Cash | Positive | Preserves purchasing power | | Commodities | Negative | Underweight | | Real Estate | Negative | Avoid leveraged plays |""", InflationRegime.LOW: """| Asset Class | Impact | Recommendation | |-------------|--------|----------------| | Equities | Neutral | Broad exposure appropriate | | Bonds | Positive | Duration acceptable | | Cash | Neutral | Modest allocation | | Commodities | Neutral | Selective exposure | | Real Estate | Neutral | Standard allocation |""", InflationRegime.TARGET: """| Asset Class | Impact | Recommendation | |-------------|--------|----------------| | Equities | Positive | Full risk allocation | | Bonds | Neutral | Balanced duration | | Cash | Negative | Minimize excess | | Commodities | Neutral | Market-weight | | Real Estate | Positive | Favorable environment |""", InflationRegime.ELEVATED: """| Asset Class | Impact | Recommendation | |-------------|--------|----------------| | Equities | Mixed | Pricing power matters | | Bonds | Negative | Short duration, TIPS | | Cash | Negative | Losing purchasing power | | Commodities | Positive | Inflation hedge | | Real Estate | Mixed | Real assets benefit |""", InflationRegime.HIGH: """| Asset Class | Impact | Recommendation | |-------------|--------|----------------| | Equities | Negative | Value, commodity producers | | Bonds | Very Negative | Avoid duration, favor TIPS | | Cash | Very Negative | Significant erosion | | Commodities | Positive | Key inflation hedge | | Real Estate | Mixed | Hard assets benefit |""", } return impacts.get(regime, "") # ============================================================================ # Macro Analyst Agent Factory # ============================================================================ def create_macro_analyst(llm): """ Create a Macro Analyst agent that specializes in: - Economic regime detection - FRED data interpretation - Yield curve analysis - Monetary policy assessment - Inflation regime classification Args: llm: Language model for generating analysis Returns: Function that processes state and returns macro analysis """ def macro_analyst_node(state): current_date = state["trade_date"] ticker = state["company_of_interest"] tools = [ get_economic_regime_analysis, get_yield_curve_analysis, get_monetary_policy_analysis, get_inflation_regime_analysis, ] system_message = """You are a specialized Macro Analyst with expertise in economic analysis and FRED data interpretation. Your role is to provide comprehensive macroeconomic assessments including: ## Your Analytical Framework ### 1. Economic Regime Detection - Classify current regime: Expansion, Late-Cycle, Contraction, Early Recovery, Stagflation, or Goldilocks - Use GDP, unemployment, inflation, and policy indicators - Identify regime transition signals ### 2. Yield Curve Analysis - Analyze 2Y-10Y and 3M-10Y spreads - Assess inversion duration and severity - Calculate recession probability - Historical context and implications ### 3. Monetary Policy Assessment - Federal Funds Rate level and trajectory - Real interest rates (nominal - inflation) - Policy stance: Hawkish, Neutral, Dovish, Emergency - Liquidity conditions (M2 growth) ### 4. Inflation Regime - CPI and PCE analysis - Inflation trajectory (accelerating/decelerating) - Implications for policy and asset classes ## Analysis Process 1. **Start with get_economic_regime_analysis** for overall regime 2. **Use get_yield_curve_analysis** for recession signals 3. **Apply get_monetary_policy_analysis** for policy stance 4. **Check get_inflation_regime_analysis** for price pressures ## Output Requirements Provide a comprehensive Macro Report including: - Current economic regime and confidence level - Key macro indicators table - Regime transition risks - Policy outlook - Asset allocation implications **Always quantify your assessments where possible.** Focus on actionable implications for trading and investment decisions. Consider how macro conditions affect the specific company under analysis.""" prompt = ChatPromptTemplate.from_messages( [ ( "system", "You are a specialized Macro Analyst assistant, collaborating with other analysts." " Use the provided macro analysis tools to assess economic conditions." " Execute comprehensive macroeconomic analysis to support trading decisions." " If you or any other assistant has the FINAL TRANSACTION PROPOSAL: **BUY/HOLD/SELL** or deliverable," " prefix your response with FINAL TRANSACTION PROPOSAL: **BUY/HOLD/SELL** so the team knows to stop." " You have access to the following tools: {tool_names}.\n{system_message}" " For your reference, the current date is {current_date}. The company we want to analyze is {ticker}.", ), MessagesPlaceholder(variable_name="messages"), ] ) prompt = prompt.partial(system_message=system_message) prompt = prompt.partial(tool_names=", ".join([tool.name for tool in tools])) prompt = prompt.partial(current_date=current_date) prompt = prompt.partial(ticker=ticker) chain = prompt | llm.bind_tools(tools) result = chain.invoke(state["messages"]) report = "" if len(result.tool_calls) == 0: report = result.content return { "messages": [result], "macro_report": report, } return macro_analyst_node