TradingAgents/tradingagents/agents/analysts/macro_analyst.py

1005 lines
39 KiB
Python

"""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