📊 Data source: FRED API (free key required)
🔧 Difficulty: Intermediate
📈 Output: Regime score + quarterly asset allocation signal
Python Investor’s Toolkit: Part 1: Momentum · Part 2: Monte Carlo · Part 3: Mean Reversion · Part 4: Earnings Quality · Part 5: Macro Regime
Part 5 of 5 — The Python Investor’s Toolkit: Practical quant tools built with free data.
Every investment strategy has a regime in which it works best. Momentum strategies thrive during expansions. Defensive positioning outperforms during slowdowns. Bond duration becomes valuable heading into recessions. The problem is most investors don’t have a systematic way to identify which regime they’re in — they rely on intuition, financial news, and lagging signals.
This final tool in the series fixes that. It downloads four key macroeconomic indicators from FRED (the Federal Reserve’s free data API), combines them into a composite regime score, and outputs a clear label: EXPANSION, LATE CYCLE, SLOWDOWN, or RECESSION RISK — along with asset allocation guidance for each.
What You’ll Need
pip install requests pandas numpy
You’ll also need a free FRED API key. Register at fred.stlouisfed.org — takes 30 seconds. The API is completely free with no usage limits for personal/research use.
💡 What FRED Contains: The St. Louis Fed’s FRED database hosts over 800,000 economic time series — including every major US and international macro indicator: interest rates, employment, inflation, trade, GDP, PMI proxies, credit spreads, and more. All free. All accessible via a clean REST API. It’s the most underused free resource in retail investing.
The Four Indicators and What They Signal
| Indicator | FRED Series ID | Bullish Signal | Bearish Signal |
|---|---|---|---|
| 10Y–2Y Yield Spread | T10Y2Y |
> 0.5% (steepening) | < 0% (inverted) |
| Unemployment Rate | UNRATE |
Stable or falling | Rising >0.3% over 3 months |
| Fed Funds Rate | FEDFUNDS |
Cutting cycle | Hiking cycle |
| Credit Spread (HY) | BAMLH0A0HYM2 |
< 4% (risk appetite) | > 6% (credit stress) |
The Code
import requests
import pandas as pd
import numpy as np
from datetime import datetime
FRED_BASE = "https://api.stlouisfed.org/fred/series/observations"
def fetch_fred(series_id, api_key, start='2005-01-01'):
"""
Fetch a time series from FRED.
Free API key at: https://fred.stlouisfed.org/docs/api/api_key.html
"""
params = {
'series_id': series_id,
'observation_start': start,
'api_key': api_key,
'file_type': 'json'
}
r = requests.get(FRED_BASE, params=params)
r.raise_for_status()
data = r.json()['observations']
df = pd.DataFrame(data)
df['date'] = pd.to_datetime(df['date'])
df['value'] = pd.to_numeric(df['value'], errors='coerce')
return df.set_index('date')[['value']].dropna()
def build_regime_model(api_key):
"""
Macro regime detector: combines yield curve, unemployment, rate cycle,
and credit spreads into a -4 to +4 composite regime score.
Score interpretation:
+3 to +4 = EXPANSION (overweight risk assets)
+1 to +2 = LATE CYCLE (reduce risk, add quality)
-1 to 0 = SLOWDOWN (defensive posture)
-2 to -4 = RECESSION RISK (capital preservation)
"""
print("Downloading macro indicators from FRED...")
# Fetch all four series
yield_spread = fetch_fred('T10Y2Y', api_key).rename(columns={'value': 'yield_spread'})
unemployment = fetch_fred('UNRATE', api_key).rename(columns={'value': 'unemployment'})
fed_funds = fetch_fred('FEDFUNDS', api_key).rename(columns={'value': 'fed_funds'})
hy_spread = fetch_fred('BAMLH0A0HYM2', api_key).rename(columns={'value': 'hy_spread'})
# Align to monthly frequency
df = (yield_spread.resample('MS').last()
.join(unemployment.resample('MS').last())
.join(fed_funds.resample('MS').last())
.join(hy_spread.resample('MS').last())
.dropna())
# ----- Signal 1: Yield Curve -----
df['yc_signal'] = np.select(
[df['yield_spread'] > 0.50, df['yield_spread'] > 0.0, df['yield_spread'] <= 0.0],
[1, 0, -1]
)
# ----- Signal 2: Unemployment Trend -----
df['unemp_3m'] = df['unemployment'].diff(3)
df['un_signal'] = np.select(
[df['unemp_3m'] < 0, df['unemp_3m'] < 0.15, df['unemp_3m'] < 0.30, True],
[1, 0, -1, -1]
)
# ----- Signal 3: Rate Cycle -----
df['rate_6m'] = df['fed_funds'].diff(6)
df['rate_signal'] = np.select(
[df['rate_6m'] < -0.50, abs(df['rate_6m']) <= 0.50, df['rate_6m'] > 0.50],
[1, 0, -1]
)
# ----- Signal 4: Credit Spreads -----
df['hy_signal'] = np.select(
[df['hy_spread'] < 4.0, df['hy_spread'] < 5.5, df['hy_spread'] < 7.0, True],
[1, 0, -1, -2]
)
# ----- Composite Score -----
df['regime_score'] = (df['yc_signal'] + df['un_signal'] +
df['rate_signal'] + df['hy_signal'])
def label(score):
if score >= 3: return 'EXPANSION'
elif score >= 1: return 'LATE CYCLE'
elif score >= -1: return 'SLOWDOWN'
else: return 'RECESSION RISK'
df['regime'] = df['regime_score'].apply(label)
# ----- Asset allocation guidance -----
guidance = {
'EXPANSION': 'Overweight equities: cyclicals, small caps, EM. Underweight bonds.',
'LATE CYCLE': 'Reduce beta. Rotate to quality/defensives. Shorten bond duration.',
'SLOWDOWN': 'Increase bond allocation. Prefer large-cap defensives. Reduce EM.',
'RECESSION RISK':'Max defensive: Treasuries, cash, gold. Minimal equity exposure.'
}
# ----- Print current regime -----
latest = df.iloc[-1]
regime = latest['regime']
print(f"n{'='*60}")
print(f" MACRO REGIME DETECTOR — {df.index[-1].strftime('%B %Y')}")
print(f"{'='*60}")
print(f" 10Y–2Y Yield Spread: {latest['yield_spread']:+.2f}% "
f"→ signal: {int(latest['yc_signal']):+d}")
print(f" Unemployment Rate: {latest['unemployment']:.1f}% "
f"(3m change: {latest['unemp_3m']:+.2f}%) "
f"→ signal: {int(latest['un_signal']):+d}")
print(f" Fed Funds Rate: {latest['fed_funds']:.2f}% "
f"(6m change: {latest['rate_6m']:+.2f}%) "
f"→ signal: {int(latest['rate_signal']):+d}")
print(f" HY Credit Spread: {latest['hy_spread']:.2f}% "
f"→ signal: {int(latest['hy_signal']):+d}")
print(f"{'='*60}")
print(f" Composite Score: {latest['regime_score']:.0f} / 4")
print(f" CURRENT REGIME: >>> {regime} <<<")
print(f" Positioning: {guidance[regime]}")
print(f"{'='*60}")
# Also print last 12 months of regime history
print(f"n Last 12 Months — Regime History:")
history = df[['yield_spread','unemployment','fed_funds',
'hy_spread','regime_score','regime']].tail(12)
print(history.to_string())
return df
# -------------------------------------------------------
# Run it — replace with your free FRED API key
# Get key at: https://fred.stlouisfed.org/docs/api/api_key.html
# -------------------------------------------------------
API_KEY = 'YOUR_FRED_API_KEY_HERE'
df = build_regime_model(API_KEY)
Connecting Regime to the Rest of the Toolkit
This tool is most powerful when used as a filter for the strategies in earlier parts of this series. Think of it as the top layer of a three-level decision framework:
| Regime | Use Momentum Scanner? | Use Mean Reversion? | Stress-Test Priority |
|---|---|---|---|
| EXPANSION | ✅ Full allocation | ✅ Works well | Monitor but lower urgency |
| LATE CYCLE | ⚠️ Reduce size | ✅ Still valid | Run and review VaR |
| SLOWDOWN | ❌ Avoid new entries | ⚠️ Defensive stocks only | Stress-test urgently |
| RECESSION RISK | ❌ Cash/bonds | ❌ Mean reversion fails | Reduce risk immediately |
📈 Key Insight: The yield spread inversion (T10Y2Y < 0) has preceded every US recession since the 1970s with a typical 12–18 month lag. But the lag makes it a poor timing tool in isolation — markets often rally strongly during the inversion period before selling off. The composite regime score’s value is that it incorporates concurrent confirmation signals (rising unemployment, widening credit spreads) that reduce false positives.
⚠️ Watch Out: Macro regime models work best as medium-term (3–6 month) positioning guides, not tactical timing tools. Regime shifts are backward-looking by construction — by the time the data confirms RECESSION RISK, markets may already be 20% lower. Use this model to set your strategic allocation each quarter, and use the momentum and mean reversion scanners from earlier in this series for shorter-term tactical decisions within that allocation.
📊 Portfolio Takeaway
Run this model monthly — or whenever yield curve, unemployment, or credit spreads make headlines. If the composite score drops from +2 to 0 or below within a single quarter, treat that as a regime shift signal: trim equity exposure by 10–15% and shorten bond duration. Don’t wait for two consecutive months of confirmation — by then you’ve missed the rotation. Use this model to set your strategic allocation each quarter, then use the momentum and mean reversion tools from earlier in this series for tactical entries within that allocation.
Completing the Toolkit
You now have five complementary tools that, used together, cover the full investment decision stack:
- Part 1 (Momentum Scanner) — which stocks to consider buying
- Part 2 (Monte Carlo) — how much downside risk your portfolio carries
- Part 3 (Mean Reversion) — short-term entry timing within trending stocks
- Part 4 (Earnings Quality) — whether a company’s financials are trustworthy
- Part 5 (Macro Regime) — what macro environment you’re operating in
All five use only free data sources — yfinance, SEC EDGAR, and FRED — and run on any machine with Python installed. The total cost: $0.
Series: The Python Investor’s Toolkit
Part 1: Momentum Scanner
Part 2: Monte Carlo Portfolio Stress-Test
Part 3: Mean Reversion Alert System
Part 4: Earnings Quality Analyzer — SEC EDGAR
Part 5: Macro Regime Detector — FRED API (this post)
