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