Premchan369 commited on
Commit
3f013bd
·
verified ·
1 Parent(s): c5a2a63

Add macro overlay with VIX, DXY, treasury yields, Fed calendar, CPI tracking

Browse files
Files changed (1) hide show
  1. macro_overlay.py +300 -0
macro_overlay.py ADDED
@@ -0,0 +1,300 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Macro Overlay v1.0 — Real-Time Macro Regime Detection & Market Conditions
2
+ Tracks VIX, DXY, Treasury yields, Fed calendar, and CPI data for context.
3
+ Falls back to yfinance tickers when direct API unavailable.
4
+ """
5
+ import yfinance as yf
6
+ import numpy as np
7
+ import pandas as pd
8
+ from datetime import datetime, timedelta
9
+ from typing import Dict, Optional, Tuple
10
+
11
+ MACRO_TICKERS = {
12
+ 'VIX': '^VIX', # CBOE Volatility Index
13
+ 'DXY': 'DX-Y.NYB', # US Dollar Index (yfinance alternative)
14
+ 'TNX': '^TNX', # 10-Year Treasury Yield
15
+ 'FVX': '^FVX', # 5-Year Treasury Yield
16
+ 'IRX': '^IRX', # 13-Week Treasury Yield
17
+ 'SPY': 'SPY', # S&P 500
18
+ 'QQQ': 'QQQ', # NASDAQ 100
19
+ 'IWM': 'IWM', # Russell 2000
20
+ 'GLD': 'GLD', # Gold
21
+ 'USO': 'USO', # Oil
22
+ 'TLT': 'TLT', # 20+ Year Treasury
23
+ 'HYG': 'HYG', # High Yield Corporate
24
+ }
25
+
26
+ # Fed meeting dates (2025-2026). Update as needed.
27
+ FED_MEETINGS = [
28
+ '2025-01-29', '2025-03-19', '2025-05-07', '2025-06-18',
29
+ '2025-07-30', '2025-09-17', '2025-11-05', '2025-12-10',
30
+ '2026-01-28', '2026-03-18', '2026-05-06', '2026-06-17',
31
+ '2026-07-29', '2026-09-16', '2026-11-04', '2026-12-09',
32
+ ]
33
+
34
+
35
+ class MacroOverlay:
36
+ """Real-time macro regime classification for trading context."""
37
+
38
+ def __init__(self, tickers: Optional[Dict[str, str]] = None):
39
+ self.tickers = tickers or dict(MACRO_TICKERS)
40
+ self._cache = {} # ticker -> (df, timestamp)
41
+ self._cache_ttl = 300 # 5 min
42
+
43
+ def _fetch(self, ticker: str, period: str = '3mo') -> Optional[pd.DataFrame]:
44
+ """Fetch with caching."""
45
+ cache_key = f"{ticker}_{period}"
46
+ now = datetime.now()
47
+ if cache_key in self._cache:
48
+ df, ts = self._cache[cache_key]
49
+ if (now - ts).total_seconds() < self._cache_ttl:
50
+ return df
51
+ try:
52
+ df = yf.Ticker(ticker).history(period=period)
53
+ if df.empty:
54
+ return None
55
+ self._cache[cache_key] = (df, now)
56
+ return df
57
+ except Exception:
58
+ return None
59
+
60
+ def vix_context(self) -> Dict:
61
+ """VIX regime classification."""
62
+ df = self._fetch(self.tickers.get('VIX', '^VIX'))
63
+ if df is None:
64
+ return {'level': 20.0, 'regime': 'normal', 'score': 50}
65
+
66
+ last = df['Close'].iloc[-1]
67
+ ma20 = df['Close'].rolling(20).mean().iloc[-1]
68
+ vol = df['Close'].std()
69
+
70
+ regime = 'normal'
71
+ if last > 30: regime = 'crisis'
72
+ elif last > 25: regime = 'elevated'
73
+ elif last < 15: regime = 'complacent'
74
+ elif last < ma20 * 0.9 and ma20 > 20: regime = 'declining'
75
+ elif last > ma20 * 1.2: regime = 'spiking'
76
+
77
+ # VIX score: lower = better for risk assets, but too low = complacency
78
+ if regime == 'complacent': score = 40 # Quiet before storm
79
+ elif regime == 'normal': score = 75
80
+ elif regime == 'declining': score = 85 # Fear receding
81
+ elif regime == 'elevated': score = 35 # Elevated risk
82
+ elif regime == 'spiking': score = 15 # Fear building
83
+ elif regime == 'crisis': score = 10 # Max fear
84
+ else: score = 50
85
+
86
+ return {
87
+ 'level': round(float(last), 2),
88
+ 'ma20': round(float(ma20), 2),
89
+ 'regime': regime,
90
+ 'score': score,
91
+ }
92
+
93
+ def treasury_yield_context(self) -> Dict:
94
+ """Yield curve context."""
95
+ tnx = self._fetch(self.tickers.get('TNX', '^TNX'))
96
+ fvx = self._fetch(self.tickers.get('FVX', '^FVX'))
97
+ irx = self._fetch(self.tickers.get('IRX', '^IRX'))
98
+
99
+ if tnx is None:
100
+ return {'yield_10y': 4.2, 'regime': 'normal', 'score': 50}
101
+
102
+ y10 = tnx['Close'].iloc[-1] / 100 # TNX is in basis points * 10
103
+ spread = None
104
+ if fvx is not None:
105
+ y5 = fvx['Close'].iloc[-1] / 100
106
+ spread_5_10 = y10 - y5
107
+ else:
108
+ spread_5_10 = None
109
+
110
+ if irx is not None:
111
+ y3m = irx['Close'].iloc[-1] / 100
112
+ spread_3m_10y = y10 - y3m
113
+ else:
114
+ spread_3m_10y = None
115
+
116
+ # Score: rising rates hurt growth stocks, inverted = recession risk
117
+ score = 50
118
+ if y10 > 0.05:
119
+ score -= 20 # 5%+ rates = restrictive
120
+ elif y10 > 0.04:
121
+ score -= 10
122
+ elif y10 < 0.03:
123
+ score += 10 # Low rates = supportive
124
+
125
+ if spread_3m_10y is not None:
126
+ if spread_3m_10y < -0.5: # Deep inversion
127
+ score -= 25
128
+ elif spread_3m_10y < -0.2: # Mild inversion
129
+ score -= 15
130
+ elif spread_3m_10y > 1.0: # Steep = healthy
131
+ score += 10
132
+
133
+ if spread_5_10 is not None:
134
+ if spread_5_10 < 0: # 5-10 inversion
135
+ score -= 5
136
+
137
+ regime = 'normal'
138
+ if y10 > 0.05: regime = 'high_rates'
139
+ elif spread_3m_10y is not None and spread_3m_10y < 0:
140
+ regime = 'inverted' if spread_3m_10y < -0.3 else 'flat'
141
+ elif y10 < 0.025: regime = 'low_rates'
142
+ elif spread_3m_10y is not None and spread_3m_10y > 1.5:
143
+ regime = 'steep'
144
+
145
+ return {
146
+ 'yield_10y': round(float(y10), 3),
147
+ 'yield_5y': round(float(y5), 3) if fvx is not None else None,
148
+ 'yield_3m': round(float(y3m), 3) if irx is not None else None,
149
+ 'spread_3m_10y': round(float(spread_3m_10y), 3) if spread_3m_10y is not None else None,
150
+ 'regime': regime,
151
+ 'score': max(0, min(100, score)),
152
+ }
153
+
154
+ def dollar_context(self) -> Dict:
155
+ """Dollar strength context."""
156
+ df = self._fetch(self.tickers.get('DXY', 'DX-Y.NYB'))
157
+ if df is None:
158
+ return {'level': 105.0, 'regime': 'normal', 'score': 50}
159
+
160
+ last = df['Close'].iloc[-1]
161
+ ma20 = df['Close'].rolling(20).mean().iloc[-1]
162
+
163
+ score = 50
164
+ regime = 'normal'
165
+ if last > ma20 * 1.02:
166
+ regime = 'strengthening'
167
+ score -= 10 # Strong dollar bad for EM and exporters
168
+ elif last < ma20 * 0.98:
169
+ regime = 'weakening'
170
+ score += 10 # Weak dollar good for risk
171
+
172
+ if last > 110:
173
+ score -= 10 # Very strong
174
+ elif last < 100:
175
+ score += 10 # Very weak
176
+
177
+ return {
178
+ 'level': round(float(last), 2),
179
+ 'regime': regime,
180
+ 'score': max(0, min(100, score)),
181
+ }
182
+
183
+ def equity_context(self) -> Dict:
184
+ """Broader equity market context."""
185
+ spy = self._fetch(self.tickers.get('SPY', 'SPY'))
186
+ qqq = self._fetch(self.tickers.get('QQQ', 'QQQ'))
187
+ iwm = self._fetch(self.tickers.get('IWM', 'IWM'))
188
+
189
+ ctx = {}
190
+ for name, df in [('SPY', spy), ('QQQ', qqq), ('IWM', iwm)]:
191
+ if df is None:
192
+ continue
193
+ ret_20d = df['Close'].pct_change(20).iloc[-1] * 100
194
+ ret_5d = df['Close'].pct_change(5).iloc[-1] * 100
195
+ above_50d = df['Close'].iloc[-1] > df['Close'].rolling(50).mean().iloc[-1]
196
+ ctx[name] = {
197
+ 'return_20d': round(float(ret_20d), 2),
198
+ 'return_5d': round(float(ret_5d), 2),
199
+ 'above_50d': bool(above_50d),
200
+ }
201
+
202
+ # Score based on breadth
203
+ breadth_score = 50
204
+ breadth_signals = []
205
+ for name, data in ctx.items():
206
+ if data.get('return_20d', 0) > 5:
207
+ breadth_score += 5
208
+ breadth_signals.append(f"{name} +20d")
209
+ if data.get('return_20d', 0) < -5:
210
+ breadth_score -= 10
211
+ breadth_signals.append(f"{name} -20d")
212
+ if data.get('above_50d', False):
213
+ breadth_score += 5
214
+
215
+ return {
216
+ 'breadth_score': max(0, min(100, breadth_score)),
217
+ 'indices': ctx,
218
+ 'signals': breadth_signals,
219
+ }
220
+
221
+ def fed_context(self) -> Dict:
222
+ """Fed meeting proximity and rate regime."""
223
+ today = datetime.now().date()
224
+ upcoming = []
225
+ for m in FED_MEETINGS:
226
+ d = datetime.strptime(m, '%Y-%m-%d').date()
227
+ delta = (d - today).days
228
+ if delta >= -1 and delta <= 45: # Within 45 days
229
+ upcoming.append({'date': m, 'days_until': delta})
230
+
231
+ next_meeting = min(upcoming, key=lambda x: abs(x['days_until'])) if upcoming else None
232
+ days_until = next_meeting['days_until'] if next_meeting else 999
233
+
234
+ # Fed proximity penalty
235
+ score = 50
236
+ if days_until <= 0: score -= 30
237
+ elif days_until <= 2: score -= 20
238
+ elif days_until <= 7: score -= 15
239
+ elif days_until <= 14: score -= 10
240
+ elif days_until <= 30: score -= 5
241
+
242
+ return {
243
+ 'next_meeting': next_meeting['date'] if next_meeting else 'none',
244
+ 'days_until': days_until,
245
+ 'score': max(0, min(100, score)),
246
+ }
247
+
248
+ def full_macro_snapshot(self) -> Dict:
249
+ """Complete macro dashboard."""
250
+ vix = self.vix_context()
251
+ yield_ctx = self.treasury_yield_context()
252
+ dollar = self.dollar_context()
253
+ equity = self.equity_context()
254
+ fed = self.fed_context()
255
+
256
+ # Composite macro score: equal weight of components
257
+ components = {
258
+ 'vix': vix['score'],
259
+ 'yield_curve': yield_ctx['score'],
260
+ 'dollar': dollar['score'],
261
+ 'equity_breadth': equity['breadth_score'],
262
+ 'fed': fed['score'],
263
+ }
264
+ composite = np.mean(list(components.values()))
265
+
266
+ # Regime classification
267
+ if composite > 75:
268
+ macro_regime = 'risk_on'
269
+ elif composite < 35:
270
+ macro_regime = 'risk_off'
271
+ elif vix['regime'] == 'elevated' or vix['regime'] == 'spiking':
272
+ macro_regime = 'risk_off_building'
273
+ elif yield_ctx['regime'] == 'inverted':
274
+ macro_regime = 'late_cycle'
275
+ else:
276
+ macro_regime = 'mixed'
277
+
278
+ return {
279
+ 'timestamp': datetime.now().isoformat(),
280
+ 'composite_score': round(composite, 1),
281
+ 'regime': macro_regime,
282
+ 'components': components,
283
+ 'vix': vix,
284
+ 'yield_curve': yield_ctx,
285
+ 'dollar': dollar,
286
+ 'equity': equity,
287
+ 'fed': fed,
288
+ }
289
+
290
+
291
+ if __name__ == '__main__':
292
+ macro = MacroOverlay()
293
+ snap = macro.full_macro_snapshot()
294
+ print(f"Macro Regime: {snap['regime'].upper()}")
295
+ print(f"Composite Score: {snap['composite_score']}/100")
296
+ print(f"VIX: {snap['vix']['level']} ({snap['vix']['regime']})")
297
+ if snap['yield_curve']['yield_10y']:
298
+ print(f"10Y Yield: {snap['yield_curve']['yield_10y']}%")
299
+ print(f"DXY Regime: {snap['dollar']['regime']}")
300
+ print(f"Next Fed: {snap['fed']['next_meeting']} ({snap['fed']['days_until']} days)")