Spaces:
Running
Running
File size: 11,646 Bytes
41cc612 727c9e5 41cc612 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 | """
fred_data.py β US/global macro indicators that drive EM India equity risk regimes.
Primary source: FRED API (fredapi library, free key at https://fred.stlouisfed.org)
Fallback: yfinance Treasury yield proxies when no FRED key is configured.
Cache: fred_macro_cache.json, 24h TTL (FRED data is daily, no intraday updates).
Usage:
from fred_data import get_fred_macro
ctx = get_fred_macro()
# ctx["risk_regime"] β "RISK_ON" | "CAUTIOUS" | "RISK_OFF"
Run standalone to test:
python fred_data.py
"""
from __future__ import annotations
import json
import logging
import os
import time
from datetime import datetime, timedelta
try:
from dotenv import load_dotenv
load_dotenv()
except ImportError:
pass
_FRED_API_KEY = os.getenv("FRED_API_KEY", "")
_CACHE_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "fred_macro_cache.json")
_CACHE_TTL_HOURS = 24
# ββ CACHE βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def _load_cache() -> dict | None:
try:
if not os.path.exists(_CACHE_FILE):
return None
with open(_CACHE_FILE, "r", encoding="utf-8") as f:
data = json.load(f)
cached_at = data.get("cached_at", "")
if cached_at:
age_hours = (time.time() - datetime.fromisoformat(cached_at).timestamp()) / 3600
if age_hours < _CACHE_TTL_HOURS:
return data
except Exception as e:
logging.debug("fred_data: cache load failed: %s", e)
return None
def _save_cache(result: dict) -> None:
try:
with open(_CACHE_FILE, "w", encoding="utf-8") as f:
json.dump(result, f)
except Exception as e:
logging.debug("fred_data: cache save failed: %s", e)
# ββ RISK SCORING ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def _compute_risk_score(
yield_spread_bps: float,
fed_rate: float,
cpi_yoy: float,
usd_strength: float,
) -> tuple[int, str]:
"""
Composite risk score 0β100 (higher = more risk-off for Indian equities).
Returns (score, regime).
"""
score = 0
# Yield curve: inversion or flattening is a leading recession indicator
if yield_spread_bps < 0:
score += 30 # inverted
elif yield_spread_bps < 50:
score += 15 # flattening
# Fed rate: high rates attract capital back to US, hurt EM flows
if fed_rate >= 5.5:
score += 25
elif fed_rate >= 4.5:
score += 15
elif fed_rate >= 3.5:
score += 8
# CPI: high US inflation keeps Fed hawkish
if cpi_yoy >= 5.0:
score += 15
elif cpi_yoy >= 3.5:
score += 7
# USD broad index: strong dollar β INR pressure β FII outflows
if usd_strength >= 110:
score += 20
elif usd_strength >= 105:
score += 10
elif usd_strength >= 102:
score += 5
score = min(100, score)
if score >= 55:
regime = "RISK_OFF"
elif score >= 28:
regime = "CAUTIOUS"
else:
regime = "RISK_ON"
return score, regime
# ββ FREDAPI FETCH βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def _fetch_via_fredapi() -> dict | None:
"""Fetch FRED series using the fredapi library. Returns None if unavailable."""
try:
import fredapi # noqa: F401
except ImportError:
logging.info("fred_data: fredapi not installed; run: pip install fredapi")
return None
if not _FRED_API_KEY:
logging.info("fred_data: FRED_API_KEY not set; skipping fredapi fetch")
return None
try:
from fredapi import Fred
fred = Fred(api_key=_FRED_API_KEY)
end = datetime.today()
start = end - timedelta(days=30)
def _latest(series_id: str) -> float | None:
try:
s = fred.get_series(series_id, observation_start=start, observation_end=end)
s = s.dropna()
return float(s.iloc[-1]) if not s.empty else None
except Exception as e:
logging.warning("fred_data: FRED series %s failed: %s", series_id, e)
return None
t10y2y = _latest("T10Y2Y") # 10Y-2Y spread (%, not bps)
fedfunds = _latest("FEDFUNDS") # Fed Funds Rate (%)
cpi = _latest("CPIAUCSL") # CPI level β need YoY %
usd = _latest("DTWEXBGS") # Broad USD index
# CPI YoY: compare to 12 months ago
cpi_yoy = None
try:
cpi_series = fred.get_series(
"CPIAUCSL",
observation_start=end - timedelta(days=400),
observation_end=end,
).dropna()
if len(cpi_series) >= 13:
latest_cpi = float(cpi_series.iloc[-1])
year_ago_cpi = float(cpi_series.iloc[-13])
cpi_yoy = round((latest_cpi / year_ago_cpi - 1) * 100, 2)
except Exception:
pass
if t10y2y is None and fedfunds is None:
return None
spread_bps = round(t10y2y * 100, 1) if t10y2y is not None else 0.0
fed_rate = round(fedfunds, 2) if fedfunds is not None else 5.25
cpi_val = round(cpi_yoy, 2) if cpi_yoy is not None else 3.5
usd_val = round(usd, 2) if usd is not None else 104.0
score, regime = _compute_risk_score(spread_bps, fed_rate, cpi_val, usd_val)
return {
"yield_curve_spread_bps": spread_bps,
"yield_curve_inverted": spread_bps < 0,
"fed_rate": fed_rate,
"cpi_yoy": cpi_val,
"usd_strength": usd_val,
"macro_risk_score": score,
"risk_regime": regime,
"source": "fredapi",
"cached_at": datetime.now().isoformat(),
}
except Exception as e:
logging.warning("fred_data: fredapi fetch failed: %s", e)
return None
# ββ YFINANCE FALLBACK βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def _fetch_via_yfinance() -> dict:
"""
Fallback: derive yield curve from yfinance Treasury tickers.
^TNX = 10-Year Treasury yield (%).
^IRX = 13-Week T-Bill yield (closest free proxy for short rates on YF).
DX-Y.NYB = USD index (DXY).
"""
try:
import yfinance as yf
except ImportError:
return _stale_fallback("yfinance not installed")
def _get_yield(ticker: str) -> float | None:
try:
raw = yf.Ticker(ticker).fast_info
price = getattr(raw, "last_price", None) or getattr(raw, "regularMarketPrice", None)
if price and float(price) > 0:
return float(price)
except Exception:
pass
# Alternative: download last 5 days
try:
df = yf.download(ticker, period="5d", progress=False, auto_adjust=True)
if not df.empty:
close = df["Close"]
if hasattr(close, "iloc"):
return float(close.dropna().iloc[-1])
except Exception:
pass
return None
t10y = _get_yield("^TNX") # 10-Year (%)
t3m = _get_yield("^IRX") # 13-Week T-Bill (%) β proxy for short end
dxy = _get_yield("DX-Y.NYB") or _get_yield("UUP") # USD index
spread_bps = 0.0
if t10y is not None and t3m is not None:
spread_bps = round((t10y - t3m) * 100, 1)
elif t10y is not None:
spread_bps = 50.0 # assume flat if only 10Y available
fed_rate = t3m if t3m is not None else 5.25
usd_val = round(dxy, 2) if dxy is not None else 104.0
cpi_yoy = 3.5 # cannot derive CPI from yfinance; use recent approximate
score, regime = _compute_risk_score(spread_bps, fed_rate, cpi_yoy, usd_val)
return {
"yield_curve_spread_bps": spread_bps,
"yield_curve_inverted": spread_bps < 0,
"fed_rate": round(fed_rate, 2),
"cpi_yoy": cpi_yoy,
"usd_strength": usd_val,
"macro_risk_score": score,
"risk_regime": regime,
"source": "yfinance_fallback",
"cached_at": datetime.now().isoformat(),
}
def _stale_fallback(reason: str) -> dict:
"""Return a neutral baseline when all data sources fail."""
logging.warning("fred_data: all sources failed (%s); returning neutral defaults", reason)
return {
"yield_curve_spread_bps": 30.0,
"yield_curve_inverted": False,
"fed_rate": 5.25,
"cpi_yoy": 3.5,
"usd_strength": 104.0,
"macro_risk_score": 28,
"risk_regime": "CAUTIOUS",
"source": "fallback",
"cached_at": datetime.now().isoformat(),
}
# ββ PUBLIC API ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def get_fred_macro(force_refresh: bool = False) -> dict:
"""
Return US/global macro indicators dict.
Keys:
yield_curve_spread_bps β 10Y-2Y (or 10Y-3M) spread in basis points
yield_curve_inverted β True if spread < 0
fed_rate β Federal Funds or short-term rate (%)
cpi_yoy β US CPI year-over-year % (FRED only; 3.5 estimate for fallback)
usd_strength β Broad USD index level (100 = Jan 2006 baseline)
macro_risk_score β 0β100 composite (higher = more risk-off)
risk_regime β "RISK_ON" | "CAUTIOUS" | "RISK_OFF"
source β "fredapi" | "yfinance_fallback" | "fallback" | "cache"
cached_at β ISO timestamp of last fetch
"""
if not force_refresh:
cached = _load_cache()
if cached:
cached["source"] = "cache"
return cached
result = _fetch_via_fredapi()
if result is None:
result = _fetch_via_yfinance()
_save_cache(result)
return result
def _regime_gate(result: dict) -> dict:
"""
Return a simplified gate dict compatible with macro_context.MacroContext.get() format.
Adds fred_risk_on key (True when regime is RISK_ON or CAUTIOUS).
"""
regime = result.get("risk_regime", "CAUTIOUS")
return {
"fred_risk_on": regime in ("RISK_ON", "CAUTIOUS"),
"fred_risk_regime": regime,
"fred_yield_inverted": result.get("yield_curve_inverted", False),
"fred_macro_risk_score": result.get("macro_risk_score", 50),
}
def get_fred_gate() -> dict:
"""Convenience wrapper returning gate-compatible dict for macro_context integration."""
return _regime_gate(get_fred_macro())
if __name__ == "__main__":
import pprint
print("Fetching US macro indicators...")
result = get_fred_macro(force_refresh=True)
pprint.pprint(result)
print(f"\nRisk regime: {result['risk_regime']} (score: {result['macro_risk_score']}/100)")
print(f"Yield curve: {'INVERTED' if result['yield_curve_inverted'] else 'NORMAL'} "
f"({result['yield_curve_spread_bps']:+.0f} bps)")
print(f"Fed rate: {result['fed_rate']:.2f}% | USD index: {result['usd_strength']:.1f}")
|