File size: 7,596 Bytes
41cc612
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
727c9e5
 
41cc612
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6ef59ab
41cc612
 
727c9e5
 
 
 
 
41cc612
 
 
 
 
 
 
 
 
6ef59ab
 
 
 
41cc612
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17ca702
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""
macro_context.py — Cross-asset macro environment for Mode C filtering.

Downloads S&P 500, USD/INR, and crude oil daily data via yfinance.
Builds boolean features lagged T-1 to prevent lookahead.
Composite gate: global_risk_on = sp500_trend AND usdinr_stable AND NOT crude_spike
"""

import yfinance as yf
import pandas as pd


class MacroContext:
    TICKERS = {
        "^GSPC":    "sp500",
        "USDINR=X": "usdinr",
        "CL=F":     "crude",
    }

    def __init__(self):
        self._features: pd.DataFrame | None = None

    def load(self, start: str, end: str) -> "MacroContext":
        frames = {}
        for ytk, name in self.TICKERS.items():
            try:
                raw = yf.download(ytk, start=start, end=end,
                                  auto_adjust=True, progress=False)
                if raw.empty:
                    raise ValueError(f"No data for {ytk}")
                close = raw["Close"]
                if isinstance(close, pd.DataFrame):
                    close = close.iloc[:, 0]
                frames[name] = close.rename(name)
            except Exception as e:
                import logging as _log
                _log.getLogger(__name__).warning("macro_context: could not download %s: %s", ytk, e)
                frames[name] = None

        available = {k: v for k, v in frames.items() if v is not None}
        if not available:
            raise RuntimeError("No macro data could be downloaded. Check internet connection.")

        self._raw = pd.DataFrame(available).sort_index()
        self._build_features()
        return self

    def _build_features(self):
        df = self._raw.copy()

        feat = pd.DataFrame(index=df.index)

        # S&P 500: 5-day momentum positive AND price above 20-day MA
        if "sp500" in df.columns:
            sp = df["sp500"]
            feat["sp500_5d_ret"]   = sp.pct_change(5)
            feat["sp500_above_ma"] = sp > sp.rolling(20).mean()
            feat["sp500_trend"]    = (feat["sp500_5d_ret"] > 0) & feat["sp500_above_ma"]
        else:
            feat["sp500_trend"] = True  # assume benign if unavailable

        # USD/INR: stable if 5-day change within ±1% (rupee not spiking)
        if "usdinr" in df.columns:
            fx = df["usdinr"]
            feat["usdinr_5d_chg"] = fx.pct_change(5) * 100
            feat["usdinr_stable"] = feat["usdinr_5d_chg"].abs() <= 1.0
        else:
            feat["usdinr_stable"] = True

        # Crude oil: no spike if 5-day change within ±5%
        if "crude" in df.columns:
            cr = df["crude"]
            feat["crude_5d_chg"]  = cr.pct_change(5) * 100
            feat["crude_spike"]   = feat["crude_5d_chg"].abs() > 5.0
        else:
            feat["crude_spike"] = False

        # Composite gate (all conditions must hold)
        feat["global_risk_on"] = (
            feat["sp500_trend"] &
            feat["usdinr_stable"] &
            ~feat["crude_spike"]
        )

        # Drop warmup rows where indicators are NaN (rolling/pct_change warmup period).
        # bool(NaN) == True in Python, so keeping these rows would cause the gate to
        # silently pass as Risk-ON during the first ~20 bars of data.
        feat = feat.dropna(subset=["sp500_trend", "usdinr_stable", "crude_spike"])

        # Lag all features by 1 trading day (use T-1 data to predict T direction)
        self._features = feat.shift(1)

    def get(self, date: pd.Timestamp) -> dict:
        if self._features is None:
            return {}
        try:
            row = self._features.loc[date]
            return {
                "sp500_trend":    bool(row.get("sp500_trend", True)),
                "usdinr_stable":  bool(row.get("usdinr_stable", True)),
                "crude_spike":    bool(row.get("crude_spike", False)),
                "global_risk_on": bool(row.get("global_risk_on", False)),
            }
        except KeyError:
            return {}

    def build_mask(self, index: pd.DatetimeIndex) -> pd.Series:
        """Return boolean Series aligned to `index` — True where global_risk_on."""
        if self._features is None:
            return pd.Series(False, index=index)
        risk_on = self._features["global_risk_on"].reindex(index, method="ffill").fillna(False)
        return risk_on.astype(bool)

    def summary(self) -> str:
        if self._features is None:
            return "MacroContext: not loaded"
        n = len(self._features)
        pct = self._features["global_risk_on"].sum() / n * 100
        return (
            f"MacroContext loaded: {n} days | "
            f"global_risk_on: {pct:.1f}% of days | "
            f"sp500_trend: {self._features['sp500_trend'].mean()*100:.1f}% | "
            f"usdinr_stable: {self._features['usdinr_stable'].mean()*100:.1f}% | "
            f"crude_spike: {self._features['crude_spike'].mean()*100:.1f}%"
        )


def load_macro(start: str = "2019-01-01", end: str = "2024-01-01") -> MacroContext:
    mc = MacroContext()
    mc.load(start, end)
    return mc


_GIFT_CACHE: dict = {"data": None, "ts": 0}
_GIFT_TTL = 900  # 15-minute cache


def get_gift_nifty_pulse() -> dict:
    """
    Fetch GIFT Nifty (^NSGIFTNIFTY) pre-market change %.

    GIFT Nifty trades in GIFT City when NSE is closed — it's the overnight
    futures proxy for where Nifty opens next session. Strong signal for
    INTRADAY and 1D predictions. Cached 15 minutes.

    Returns dict with keys: price, prev_close, change_pct, direction, source.
    direction is BULLISH (>+0.2%), BEARISH (<-0.2%), or NEUTRAL.
    """
    import time
    global _GIFT_CACHE
    now = time.time()
    if _GIFT_CACHE["data"] and (now - _GIFT_CACHE["ts"]) < _GIFT_TTL:
        return _GIFT_CACHE["data"]

    try:
        ticker = yf.Ticker("^NSGIFTNIFTY")
        fi = ticker.fast_info
        price = getattr(fi, "last_price", None) or getattr(fi, "regular_market_price", None)
        prev_close = getattr(fi, "previous_close", None) or getattr(fi, "regular_market_previous_close", None)
        if price and prev_close and float(prev_close) > 0:
            change_pct = (float(price) / float(prev_close) - 1) * 100
        else:
            df = yf.download("^NSGIFTNIFTY", period="2d", progress=False, auto_adjust=True)
            if len(df) >= 2:
                closes = df["Close"].squeeze()
                price = float(closes.iloc[-1])
                prev_close = float(closes.iloc[-2])
                change_pct = (price / prev_close - 1) * 100
            else:
                change_pct = 0.0
                price = prev_close = None

        direction = "BULLISH" if change_pct > 0.2 else ("BEARISH" if change_pct < -0.2 else "NEUTRAL")
        result = {
            "price": round(float(price), 2) if price else None,
            "prev_close": round(float(prev_close), 2) if prev_close else None,
            "change_pct": round(change_pct, 2),
            "direction": direction,
            "source": "yfinance",
        }
        _GIFT_CACHE = {"data": result, "ts": now}
        return result
    except Exception as exc:
        result = {
            "price": None, "prev_close": None, "change_pct": 0.0,
            "direction": "NEUTRAL", "source": "error", "error": str(exc),
        }
        _GIFT_CACHE = {"data": result, "ts": now - _GIFT_TTL + 60}
        return result


if __name__ == "__main__":
    print("Testing MacroContext download...")
    ctx = load_macro()
    print(ctx.summary())
    # Spot-check one date
    test_date = pd.Timestamp("2022-03-10")
    print(f"Sample date {test_date.date()}: {ctx.get(test_date)}")