JayLacoma commited on
Commit
c87fed3
·
verified ·
1 Parent(s): 6a71d0d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +263 -277
app.py CHANGED
@@ -10,372 +10,358 @@ import os
10
 
11
  warnings.filterwarnings('ignore')
12
 
13
- # Import data engine (ensure file is named `geo_macro.py`)
14
  from geo_macro import UnifiedMarketDataDownloader
15
 
16
  # ======================
17
  # CONFIGURATION
18
  # ======================
19
-
20
  DATA_FILE = 'unified_market_data.csv'
21
  CACHE_HOURS = 24
22
 
23
- # Modern dark theme
24
  COLORS = {
25
- 'primary': '#00D9FF',
26
- 'secondary': '#FF6B9D',
27
- 'accent': '#00FFAA',
28
- 'warning': '#FFB800',
29
- 'danger': '#FF3864',
30
- 'success': '#00FF88',
31
- 'bg_dark': '#0A0E27',
32
- 'bg_card': '#151932',
33
- 'grid': '#2a2e45'
34
  }
35
 
36
- # Securely load FRED API key from environment (set as Secret in HF Spaces)
37
  FRED_API_KEY = os.getenv("FRED_API_KEY")
38
  if not FRED_API_KEY:
39
- print("⚠️ Warning: FRED_API_KEY not set. Economic data will be skipped.")
40
 
41
  # ======================
42
- # DATA LOADING (File-based only)
43
  # ======================
44
-
45
  def load_or_download_data():
46
- """Load from CSV or download if missing"""
47
- if os.path.exists(DATA_FILE):
48
- file_time = datetime.fromtimestamp(os.path.getmtime(DATA_FILE))
49
- if datetime.now() - file_time < timedelta(hours=CACHE_HOURS):
50
- print(f"📦 Loading cached data from {DATA_FILE}")
51
- return pd.read_csv(DATA_FILE, index_col=0, parse_dates=True)
52
-
53
- print("🔄 Downloading fresh market data...")
54
  downloader = UnifiedMarketDataDownloader(fred_api_key=FRED_API_KEY)
55
  df = downloader.download_all_data(start_date='2018-01-01')
56
  df.to_csv(DATA_FILE)
57
  print(f"💾 Saved to {DATA_FILE}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  return df
59
 
60
  # ======================
61
- # FEATURE ENGINEERING
62
  # ======================
 
 
 
 
 
 
 
 
 
 
63
 
64
- def add_thematic_features(df):
65
  THEMES = {
66
- "AI & Datacenters": ["Technology", "SMH", "SKYY", "BOTZ", "Cloud_Computing"],
67
- "Defense & Security": ["ITA", "XAR", "HACK", "Aerospace_Defense", "Defense_Stocks"],
68
- "Nuclear Renaissance": ["URA", "Energy", "Utilities", "Energy_Security"],
69
- "China Stress": ["KWEB", "FXI", "CNY", "China", "China_Tech"],
70
- "Commodity Inflation": ["DBA", "DBB", "Oil", "Copper", "Gold", "Agricultural"],
71
- "Gold & Safe Havens": ["Gold", "Gold_Safe_Haven", "TLT", "JPY", "CHF", "Gold_Miners"],
72
- "Early Cycle": ["Small_Cap_Value", "XHB", "Homebuilders", "Regional_Banks"],
73
- "Late Cycle": ["High_Dividend", "Utilities", "Consumer_Staples", "Value_Stocks"],
74
- "Credit Stress": ["Emerging_Market_Debt", "HYG", "Leveraged_Loans", "JNK"],
75
- "Liquidity Conditions": ["M2", "WALCL", "Short_Term_Treasuries", "Preferred_Stock"]
76
  }
77
-
78
- df = df.copy()
79
  for name, assets in THEMES.items():
80
- available = [a for a in assets if a in df.columns]
81
  if available:
82
- returns = df[available].pct_change()
83
- mom = returns.mean(axis=1).rolling(60, min_periods=30).sum()
84
- mean = mom.rolling(500, min_periods=100).mean()
85
- std = mom.rolling(500, min_periods=100).std()
86
- df[f"{name}_Z"] = (mom - mean) / std
87
- else:
88
- df[f"{name}_Z"] = np.nan
89
- return df
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
 
91
  def get_processed_data():
 
92
  df = load_or_download_data()
93
- return add_thematic_features(df)
94
 
95
  # ======================
96
- # PLOT THEME
97
  # ======================
98
-
99
- def modern_layout(title):
100
- return dict(
101
- plot_bgcolor=COLORS['bg_dark'],
102
- paper_bgcolor=COLORS['bg_card'],
103
- font=dict(color='white', size=13),
104
- title=dict(text=title, font=dict(size=22, color=COLORS['accent']), x=0.5),
105
- xaxis=dict(gridcolor=COLORS['grid'], showgrid=True),
106
- yaxis=dict(gridcolor=COLORS['grid'], showgrid=True),
107
- hovermode='x unified'
 
 
108
  )
109
 
110
- # ======================
111
- # PLOT FUNCTIONS (FIXED)
112
- # ======================
113
-
114
- def plot_regime_dashboard(start_date, end_date):
115
- df = get_processed_data()
116
- df = df[(df.index >= pd.to_datetime(start_date)) & (df.index <= pd.to_datetime(end_date))]
117
-
118
- z_cols = [col for col in df.columns if col.endswith('_Z')]
119
- if not z_cols:
120
- return go.Figure()
121
-
122
- clean_names = [col.replace('_Z', '').replace('_', ' ') for col in z_cols]
123
- heatmap_data = df[z_cols].fillna(0)
124
-
125
  fig = go.Figure(go.Heatmap(
126
- z=heatmap_data.T.values,
127
- x=heatmap_data.index,
128
- y=clean_names,
129
- colorscale='RdBu_r',
130
- zmid=0,
131
- zmin=-3,
132
- zmax=3,
133
- colorbar=dict(title="Z-Score") # ✅ FIXED: removed 'titleside'
134
  ))
135
- fig.update_layout(**modern_layout("🌍 Thematic Regime Heatmap"), height=600)
136
  return fig
137
 
138
- def plot_thematic_pulse(start_date, end_date):
139
- df = get_processed_data()
140
- df = df[(df.index >= pd.to_datetime(start_date)) & (df.index <= pd.to_datetime(end_date))]
141
-
142
- theme_names = [
143
- "AI & Datacenters", "Defense & Security", "Nuclear Renaissance",
144
- "China Stress", "Commodity Inflation", "Gold & Safe Havens",
145
- "Early Cycle", "Late Cycle", "Credit Stress", "Liquidity Conditions"
146
- ]
147
- z_cols = [f"{name}_Z" for name in theme_names if f"{name}_Z" in df.columns]
148
-
149
- if not z_cols:
150
- return go.Figure()
151
-
152
- latest = df[z_cols].iloc[-1].dropna()
153
- clean_names = [col.replace('_Z', '').replace('_', ' ') for col in latest.index]
154
- latest.index = clean_names
155
-
156
- colors = [
157
- COLORS['danger'] if x < -1.5 else
158
- COLORS['warning'] if x < -0.5 else
159
- COLORS['success'] if x > 1.5 else
160
- COLORS['primary'] if x > 0.5 else
161
- '#555'
162
- for x in latest
163
- ]
 
 
 
 
 
 
 
 
 
 
164
 
165
- fig = go.Figure(go.Bar(
166
- x=latest.values, y=latest.index, orientation='h',
167
- marker_color=colors, text=[f"{x:.2f}" for x in latest.values],
168
- textposition='outside'
169
- ))
170
- fig.update_layout(**modern_layout("🔥 Current Thematic Pulse"), height=600, xaxis_title="60-Day Momentum Z-Score")
171
  return fig
172
 
173
  def plot_multi_asset_performance(start_date, end_date, assets):
174
- df = get_processed_data()
175
- df = df[(df.index >= pd.to_datetime(start_date)) & (df.index <= pd.to_datetime(end_date))]
176
  available = [a for a in assets if a in df.columns]
177
- if not available:
178
- return go.Figure()
179
-
180
  fig = go.Figure()
181
  for asset in available:
182
  prices = df[asset].dropna()
183
- if len(prices) > 0:
184
  norm = (prices / prices.iloc[0]) * 100
185
  fig.add_trace(go.Scatter(x=norm.index, y=norm, mode='lines', name=asset))
186
-
187
- fig.update_layout(**modern_layout("📈 Multi-Asset Performance (Normalized)"), height=600, yaxis_title="Index (Base = 100)")
188
  return fig
189
 
190
  def plot_correlation_heatmap(start_date, end_date, assets):
191
- df = get_processed_data()
192
- df = df[(df.index >= pd.to_datetime(start_date)) & (df.index <= pd.to_datetime(end_date))]
193
  available = [a for a in assets if a in df.columns]
194
- if len(available) < 2:
195
- return go.Figure()
196
-
197
  corr = df[available].pct_change().corr()
198
  fig = go.Figure(go.Heatmap(
199
- z=corr.values,
200
- x=corr.columns,
201
- y=corr.columns,
202
- colorscale='RdBu_r',
203
- zmid=0,
204
- text=np.round(corr.values, 2),
205
- texttemplate='%{text}',
206
- colorbar=dict(title="Correlation") # ✅ FIXED: removed 'titleside'
207
  ))
208
- fig.update_layout(**modern_layout("🔗 Asset Correlation Matrix"), height=650, width=750)
209
  return fig
210
 
211
  def plot_drawdown_analysis(start_date, end_date, assets):
212
- df = get_processed_data()
213
- df = df[(df.index >= pd.to_datetime(start_date)) & (df.index <= pd.to_datetime(end_date))]
214
  available = [a for a in assets if a in df.columns]
215
- if not available:
216
- return go.Figure()
217
-
218
- fig = make_subplots(rows=2, cols=1, shared_xaxes=True,
219
- subplot_titles=('Cumulative Performance', 'Drawdown'),
220
- vertical_spacing=0.08, row_heights=[0.6, 0.4])
221
 
 
 
222
  for asset in available:
223
  prices = df[asset].dropna()
224
- if len(prices) > 0:
225
  cum = (prices / prices.iloc[0]) * 100
226
- drawdown = ((cum - cum.expanding().max()) / cum.expanding().max()) * 100
 
227
  fig.add_trace(go.Scatter(x=cum.index, y=cum, mode='lines', name=asset), row=1, col=1)
228
- fig.add_trace(go.Scatter(x=drawdown.index, y=drawdown, mode='lines', fill='tozeroy', showlegend=False), row=2, col=1)
229
-
230
- fig.update_layout(**modern_layout("📉 Drawdown Analysis"), height=800)
231
- fig.update_xaxes(title_text="Date", row=2, col=1)
232
- fig.update_yaxes(title_text="Index (Base = 100)", row=1, col=1)
233
  fig.update_yaxes(title_text="Drawdown (%)", row=2, col=1)
234
  return fig
235
 
236
- def plot_rolling_sharpe(start_date, end_date, assets, window=252):
237
- df = get_processed_data()
238
- df = df[(df.index >= pd.to_datetime(start_date)) & (df.index <= pd.to_datetime(end_date))]
239
- available = [a for a in assets if a in df.columns]
240
- if not available:
241
- return go.Figure()
242
-
243
- fig = go.Figure()
244
- for asset in available:
245
- ret = df[asset].pct_change().dropna()
246
- if len(ret) > window:
247
- sharpe = (ret.rolling(window).mean() * 252) / (ret.rolling(window).std() * np.sqrt(252))
248
- fig.add_trace(go.Scatter(x=sharpe.index, y=sharpe, mode='lines', name=asset))
249
-
250
- fig.add_hline(y=0, line_dash="dash", line_color="gray")
251
- fig.add_hline(y=1, line_dash="dot", line_color=COLORS['success'])
252
- fig.update_layout(**modern_layout(f"📊 Rolling Sharpe Ratio ({window//252}Y)"), height=600, yaxis_title="Sharpe Ratio")
253
- return fig
254
-
255
  def plot_sector_rotation(start_date, end_date):
256
- df = get_processed_data()
257
- df = df[(df.index >= pd.to_datetime(start_date)) & (df.index <= pd.to_datetime(end_date))]
258
- sectors = ['Technology', 'Financials', 'Healthcare', 'Consumer_Discretionary',
259
- 'Consumer_Staples', 'Energy', 'Materials', 'Industrials', 'Utilities',
260
- 'Real_Estate', 'Communication_Services']
261
  available = [s for s in sectors if s in df.columns]
262
- if not available:
263
- return go.Figure()
264
-
265
- momentum = {s: df[s].pct_change(60).iloc[-1] * 100 for s in available}
266
- fig = go.Figure(go.Scatterpolar(
267
- r=list(momentum.values()),
268
- theta=[s.replace('_', ' ') for s in momentum.keys()],
269
- fill='toself',
270
- line_color=COLORS['primary']
271
  ))
272
  fig.update_layout(
273
- **modern_layout("🎯 Sector Rotation (3M Momentum %)"),
274
- height=650,
275
  polar=dict(
276
- radialaxis=dict(visible=True, gridcolor=COLORS['grid'], range=[-10, 10]),
277
  angularaxis=dict(gridcolor=COLORS['grid'])
278
  )
279
  )
280
  return fig
281
 
282
- def plot_risk_dashboard(start_date, end_date):
283
- df = get_processed_data()
284
- df = df[(df.index >= pd.to_datetime(start_date)) & (df.index <= pd.to_datetime(end_date))]
285
- risk_assets = ['VIX', 'HYG', 'T10Y2Y', 'DXY', 'Gold']
286
- available = [a for a in risk_assets if a in df.columns]
287
- if not available:
288
- return go.Figure()
289
-
290
- fig = make_subplots(rows=len(available), cols=1, shared_xaxes=True,
291
- subplot_titles=[a.replace('_', ' ') for a in available],
292
- vertical_spacing=0.06)
293
-
294
- for i, asset in enumerate(available, 1):
295
- prices = df[asset].dropna()
296
- if len(prices) > 0:
297
- fig.add_trace(go.Scatter(x=prices.index, y=prices, mode='lines', line_color=COLORS['primary']), row=i, col=1)
298
-
299
- fig.update_layout(**modern_layout("⚠️ Risk Indicators Dashboard"), height=220 * len(available), showlegend=False)
300
- return fig
301
-
302
  # ======================
303
- # GRADIO UI
304
  # ======================
305
-
306
  custom_css = """
307
- .gradio-container { background: linear-gradient(135deg, #0A0E27 0%, #151932 100%) !important; }
308
- .tabs { border-radius: 12px; margin-top: 10px; }
309
- button { border-radius: 8px !important; font-weight: 600 !important; }
310
- .group { border-radius: 12px !important; background: #1a1f3a !important; padding: 16px !important; }
 
 
 
 
 
311
  """
312
 
313
- # Use static list to avoid runtime data dependency
314
  COMMON_TICKERS = [
315
- 'SP500', 'NASDAQ', 'DJI', 'VIX', 'Gold', 'Oil', 'TLT', 'HYG', 'LQD', 'DGS10', 'DGS2',
316
- 'DXY', 'EURUSD', 'JPYUSD', 'Bitcoin', 'China', 'Europe', 'Japan', 'India',
317
- 'Technology', 'Financials', 'Energy', 'Healthcare', 'Utilities', 'SMH', 'KWEB',
318
- 'ITA', 'URA', 'REMX', 'XLE', 'GLD', 'M2', 'UNRATE', 'CPIAUCSL', 'T10Y2Y'
319
- ]
320
-
321
- with gr.Blocks(title="Macro-Thematic Intelligence Platform", css=custom_css, theme=gr.themes.Base()) as demo:
322
- gr.Markdown("# 🌐 Macro-Thematic Intelligence Platform\n### Detect Regimes • Track Themes • Monitor Risk")
323
-
324
- with gr.Tabs():
325
- with gr.Tab("🌍 Regime Dashboard"):
326
- with gr.Row():
327
- s1 = gr.Textbox("2023-01-01", label="Start Date")
328
- e1 = gr.Textbox(datetime.today().strftime('%Y-%m-%d'), label="End Date")
329
- gr.Button("🔄 Update", variant="primary").click(plot_regime_dashboard, [s1, e1], gr.Plot())
330
-
331
- with gr.Tab("🔥 Thematic Pulse"):
332
- with gr.Row():
333
- s2 = gr.Textbox("2023-01-01", label="Start Date")
334
- e2 = gr.Textbox(datetime.today().strftime('%Y-%m-%d'), label="End Date")
335
- gr.Button("🔄 Analyze", variant="primary").click(plot_thematic_pulse, [s2, e2], gr.Plot())
336
-
337
- with gr.Tab("📈 Performance"):
338
- with gr.Row():
339
- s3 = gr.Textbox("2023-01-01", label="Start Date")
340
- e3 = gr.Textbox(datetime.today().strftime('%Y-%m-%d'), label="End Date")
341
- assets1 = gr.Dropdown(COMMON_TICKERS, value=['SP500', 'Gold', 'TLT', 'Bitcoin'], multiselect=True, label="Assets")
342
- gr.Button("📊 Plot", variant="primary").click(plot_multi_asset_performance, [s3, e3, assets1], gr.Plot())
343
-
344
- with gr.Tab("🔗 Correlations"):
345
- with gr.Row():
346
- s4 = gr.Textbox("2023-01-01", label="Start Date")
347
- e4 = gr.Textbox(datetime.today().strftime('%Y-%m-%d'), label="End Date")
348
- assets2 = gr.Dropdown(COMMON_TICKERS, value=['SP500', 'Gold', 'TLT', 'DXY', 'VIX'], multiselect=True, label="Assets")
349
- gr.Button("🔍 Analyze", variant="primary").click(plot_correlation_heatmap, [s4, e4, assets2], gr.Plot())
350
-
351
- with gr.Tab("📉 Drawdowns"):
352
- with gr.Row():
353
- s5 = gr.Textbox("2023-01-01", label="Start Date")
354
- e5 = gr.Textbox(datetime.today().strftime('%Y-%m-%d'), label="End Date")
355
- assets3 = gr.Dropdown(COMMON_TICKERS, value=['SP500', 'NASDAQ', 'Gold', 'Bitcoin'], multiselect=True, label="Assets")
356
- gr.Button("📉 Analyze", variant="primary").click(plot_drawdown_analysis, [s5, e5, assets3], gr.Plot())
357
-
358
- with gr.Tab("📊 Sharpe Ratio"):
359
  with gr.Row():
360
- s6 = gr.Textbox("2023-01-01", label="Start Date")
361
- e6 = gr.Textbox(datetime.today().strftime('%Y-%m-%d'), label="End Date")
362
- assets4 = gr.Dropdown(COMMON_TICKERS, value=['SP500', 'TLT', 'Gold'], multiselect=True, label="Assets")
363
- window = gr.Slider(60, 504, value=252, step=21, label="Rolling Window (days)")
364
- gr.Button("📈 Calculate", variant="primary").click(plot_rolling_sharpe, [s6, e6, assets4, window], gr.Plot())
365
-
366
- with gr.Tab("🎯 Sectors"):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
367
  with gr.Row():
368
- s7 = gr.Textbox("2023-01-01", label="Start Date")
369
- e7 = gr.Textbox(datetime.today().strftime('%Y-%m-%d'), label="End Date")
370
- gr.Button("🔄 Analyze", variant="primary").click(plot_sector_rotation, [s7, e7], gr.Plot())
371
-
372
- with gr.Tab("⚠️ Risk Monitor"):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
373
  with gr.Row():
374
- s8 = gr.Textbox("2023-01-01", label="Start Date")
375
- e8 = gr.Textbox(datetime.today().strftime('%Y-%m-%d'), label="End Date")
376
- gr.Button("🚨 Load", variant="primary").click(plot_risk_dashboard, [s8, e8], gr.Plot())
377
-
378
- gr.Markdown("---\n**Data**: Yahoo Finance + FRED | **Theme**: Nonlinear Regime Detection")
 
 
 
 
 
 
 
 
 
 
 
 
379
 
380
  if __name__ == "__main__":
381
- demo.launch()
 
10
 
11
  warnings.filterwarnings('ignore')
12
 
13
+ # Assume geo_macro.py exists and works as intended.
14
  from geo_macro import UnifiedMarketDataDownloader
15
 
16
  # ======================
17
  # CONFIGURATION
18
  # ======================
 
19
  DATA_FILE = 'unified_market_data.csv'
20
  CACHE_HOURS = 24
21
 
 
22
  COLORS = {
23
+ 'primary': '#2E4053', # Dark Slate Gray for main plots
24
+ 'secondary': '#85929E', # Lighter Gray for secondary elements
25
+ 'accent': '#17202A', # Nearly Black for titles
26
+ 'grid': '#EAECEE', # Very Light Gray for grids
27
+ 'bg_primary': '#FFFFFF', # White background
28
+ 'bg_secondary': '#F8F9F9',# Off-white for cards/tabs
29
+ 'success': '#27AE60', # Green
30
+ 'danger': '#C0392B', # Red
31
+ 'warning': '#F39C12' # Yellow
32
  }
33
 
34
+ # Securely load FRED API key
35
  FRED_API_KEY = os.getenv("FRED_API_KEY")
36
  if not FRED_API_KEY:
37
+ print("⚠️ Warning: FRED_API_KEY not set. Economic data might be limited.")
38
 
39
  # ======================
40
+ # DATA LOADING & MOCKING (if geo_macro.py is not available)
41
  # ======================
 
42
  def load_or_download_data():
 
 
 
 
 
 
 
 
43
  downloader = UnifiedMarketDataDownloader(fred_api_key=FRED_API_KEY)
44
  df = downloader.download_all_data(start_date='2018-01-01')
45
  df.to_csv(DATA_FILE)
46
  print(f"💾 Saved to {DATA_FILE}")
47
+ except (ImportError, ModuleNotFoundError):
48
+ print("⚠️ `geo_macro.py` not found. Generating mock data.")
49
+ dates = pd.date_range(start='2018-01-01', end=datetime.today(), freq='B')
50
+ tickers = ['SP500', 'NASDAQ', 'VIX', 'Gold', 'Oil', 'TLT', 'HYG', 'DXY',
51
+ 'T10Y2Y', 'CPIAUCSL', 'ITA', 'MTUM', 'VTV', 'QUAL', 'IJR',
52
+ 'Technology', 'Financials', 'Healthcare', 'Consumer_Discretionary',
53
+ 'Consumer_Staples', 'Energy', 'Materials', 'Industrials', 'Utilities']
54
+ data = {ticker: (100 + np.random.randn(len(dates)).cumsum() * 0.5) for ticker in tickers}
55
+ df = pd.DataFrame(data, index=dates)
56
+ # Make some series more realistic
57
+ df['VIX'] = np.random.uniform(10, 40, size=len(dates))
58
+ df['T10Y2Y'] = np.random.randn(len(dates)) * 0.5
59
+ df['CPIAUCSL'] = (3 + np.random.randn(len(dates)).cumsum() * 0.01)
60
+ df.to_csv(DATA_FILE)
61
+ print(f"💾 Mock data saved to {DATA_FILE}")
62
  return df
63
 
64
  # ======================
65
+ # ADVANCED FEATURE ENGINEERING
66
  # ======================
67
+ def calculate_z_score(series, fast_window=60, slow_window=252):
68
+ """Calculates a rolling z-score of momentum."""
69
+ momentum = series.pct_change(fast_window)
70
+ mean = momentum.rolling(slow_window).mean()
71
+ std = momentum.rolling(slow_window).std()
72
+ return (momentum - mean) / std
73
+
74
+ def add_thematic_and_factor_features(df):
75
+ """Engineer features for themes, factors, and custom indices."""
76
+ df_out = df.copy()
77
 
78
+ # 1. Thematic Baskets (Momentum Z-Score)
79
  THEMES = {
80
+ "AI & Tech": ["Technology", "NASDAQ", "SMH"],
81
+ "Defense & Security": ["ITA", "XAR"],
82
+ "Inflationary Pressures": ["DBA", "DBB", "Oil", "Copper", "Energy"],
83
+ "Safe Havens": ["Gold", "TLT", "CHF"],
84
+ "Credit & Liquidity Stress": ["HYG", "JNK", "T10Y2Y"],
 
 
 
 
 
85
  }
 
 
86
  for name, assets in THEMES.items():
87
+ available = [a for a in assets if a in df_out.columns]
88
  if available:
89
+ # Use z-score of price level for non-mean-reverting themes
90
+ theme_series = df_out[available].mean(axis=1)
91
+ df_out[f"{name}_Z"] = calculate_z_score(theme_series)
92
+
93
+ # 2. Factor Baskets (e.g., Fama-French proxies)
94
+ FACTORS = {
95
+ "Momentum": ["MTUM"], "Value": ["VTV"], "Quality": ["QUAL"],
96
+ }
97
+ for name, assets in FACTORS.items():
98
+ if assets[0] in df_out.columns:
99
+ df_out[f"Factor_{name}_Z"] = calculate_z_score(df_out[assets[0]])
100
+ if 'IJR' in df_out.columns and 'SP500' in df_out.columns:
101
+ size_premium = df_out['IJR'].pct_change() - df_out['SP500'].pct_change()
102
+ df_out["Factor_Size_Premium_Z"] = calculate_z_score(size_premium.cumsum() + 1)
103
+
104
+
105
+ # 3. Custom Geopolitical Risk Index
106
+ geo_assets = ['Oil', 'Gold', 'ITA', 'DXY']
107
+ available_geo = [a for a in geo_assets if a in df_out.columns]
108
+ if len(available_geo) > 1:
109
+ norm_geo = df_out[available_geo].dropna().apply(lambda x: (x / x.iloc[0]))
110
+ geo_index = norm_geo.mean(axis=1)
111
+ df_out['Geopolitical_Risk_Z'] = calculate_z_score(geo_index, fast_window=21) # More sensitive
112
+
113
+ return df_out
114
 
115
  def get_processed_data():
116
+ """Main data pipeline function."""
117
  df = load_or_download_data()
118
+ return add_thematic_and_factor_features(df)
119
 
120
  # ======================
121
+ # PLOTTING & AESTHETICS
122
  # ======================
123
+ def monochrome_layout(title, height=500):
124
+ """Creates a professional, monochrome plot layout."""
125
+ return go.Layout(
126
+ title=dict(text=title, font=dict(color=COLORS['accent'], size=20), x=0.5),
127
+ plot_bgcolor=COLORS['bg_primary'],
128
+ paper_bgcolor=COLORS['bg_primary'],
129
+ font=dict(color=COLORS['primary'], size=12),
130
+ xaxis=dict(gridcolor=COLORS['grid'], linecolor=COLORS['secondary']),
131
+ yaxis=dict(gridcolor=COLORS['grid'], linecolor=COLORS['secondary']),
132
+ hovermode='x unified',
133
+ legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1),
134
+ height=height
135
  )
136
 
137
+ def plot_heatmap(df, title, colorscale='RdBu_r', zmid=0):
138
+ """Generic heatmap plotting function."""
 
 
 
 
 
 
 
 
 
 
 
 
 
139
  fig = go.Figure(go.Heatmap(
140
+ z=df.T.values,
141
+ x=df.index,
142
+ y=[col.replace('_Z', '').replace('_', ' ') for col in df.columns],
143
+ colorscale=colorscale, zmid=zmid, zmin=-2.5, zmax=2.5,
144
+ colorbar=dict(title="Z-Score")
 
 
 
145
  ))
146
+ fig.update_layout(monochrome_layout(title, height=500))
147
  return fig
148
 
149
+ # --- PLOT FUNCTIONS ---
150
+
151
+ def plot_thematic_regime(start_date, end_date):
152
+ df = get_processed_data().loc[start_date:end_date]
153
+ z_cols = [c for c in df.columns if '_Z' in c and 'Factor' not in c and 'Geopolitical' not in c]
154
+ if not z_cols: return go.Figure().update_layout(monochrome_layout("No Thematic Data Available"))
155
+ return plot_heatmap(df[z_cols], "🌍 Thematic Regime Heatmap")
156
+
157
+ def plot_factor_regime(start_date, end_date):
158
+ df = get_processed_data().loc[start_date:end_date]
159
+ z_cols = [c for c in df.columns if 'Factor' in c]
160
+ if not z_cols: return go.Figure().update_layout(monochrome_layout("No Factor Data Available"))
161
+ return plot_heatmap(df[z_cols], "🔭 Factor Performance Heatmap")
162
+
163
+ def plot_macro_dashboard(start_date, end_date):
164
+ df = get_processed_data().loc[start_date:end_date]
165
+ indicators = {'CPIAUCSL': 'YoY Inflation (%)', 'T10Y2Y': 'Yield Curve (10Y-2Y)',
166
+ 'VIX': 'Volatility Index', 'DXY': 'US Dollar Index'}
167
+ available = {k: v for k, v in indicators.items() if k in df.columns}
168
+ if not available: return go.Figure().update_layout(monochrome_layout("No Macro Data Available"))
169
+
170
+ fig = make_subplots(rows=len(available), cols=1, shared_xaxes=True,
171
+ subplot_titles=list(available.values()), vertical_spacing=0.1)
172
+ for i, (ticker, title) in enumerate(available.items(), 1):
173
+ series = df[ticker].dropna()
174
+ if ticker == 'CPIAUCSL': # Calculate YoY % change for CPI
175
+ series = series.pct_change(252) * 100
176
+ fig.add_trace(go.Scatter(x=series.index, y=series, mode='lines',
177
+ line=dict(color=COLORS['primary'], width=2), name=ticker), row=i, col=1)
178
+ fig.update_layout(monochrome_layout("📈 Key Macroeconomic Indicators"), height=200 * len(available), showlegend=False)
179
+ return fig
180
+
181
+ def plot_geopolitical_risk(start_date, end_date):
182
+ df = get_processed_data().loc[start_date:end_date]
183
+ if 'Geopolitical_Risk_Z' not in df.columns:
184
+ return go.Figure().update_layout(monochrome_layout("Geopolitical Risk Index Not Available"))
185
 
186
+ risk_series = df['Geopolitical_Risk_Z'].dropna()
187
+ fig = go.Figure(go.Scatter(x=risk_series.index, y=risk_series, mode='lines',
188
+ fill='tozeroy', line_color=COLORS['danger']))
189
+ fig.add_hline(y=0, line_dash="dash", line_color=COLORS['secondary'])
190
+ fig.add_hline(y=1.5, line_dash="dot", line_color=COLORS['warning'])
191
+ fig.update_layout(monochrome_layout("💥 Geopolitical Risk Index (Z-Score)"), yaxis_title="Momentum Z-Score")
192
  return fig
193
 
194
  def plot_multi_asset_performance(start_date, end_date, assets):
195
+ df = get_processed_data().loc[start_date:end_date]
 
196
  available = [a for a in assets if a in df.columns]
197
+ if not available: return go.Figure().update_layout(monochrome_layout("Select assets to plot"))
198
+
 
199
  fig = go.Figure()
200
  for asset in available:
201
  prices = df[asset].dropna()
202
+ if not prices.empty:
203
  norm = (prices / prices.iloc[0]) * 100
204
  fig.add_trace(go.Scatter(x=norm.index, y=norm, mode='lines', name=asset))
205
+ fig.update_layout(monochrome_layout("📊 Multi-Asset Performance (Normalized)"),
206
+ yaxis_title="Index (Base = 100)")
207
  return fig
208
 
209
  def plot_correlation_heatmap(start_date, end_date, assets):
210
+ df = get_processed_data().loc[start_date:end_date]
 
211
  available = [a for a in assets if a in df.columns]
212
+ if len(available) < 2: return go.Figure().update_layout(monochrome_layout("Select 2+ assets"))
213
+
 
214
  corr = df[available].pct_change().corr()
215
  fig = go.Figure(go.Heatmap(
216
+ z=corr.values, x=corr.columns, y=corr.columns,
217
+ colorscale='RdBu_r', zmid=0,
218
+ text=np.round(corr.values, 2), texttemplate='%{text}',
219
+ colorbar=dict(title="Correlation")
 
 
 
 
220
  ))
221
+ fig.update_layout(monochrome_layout("🔗 Asset Correlation Matrix", height=600))
222
  return fig
223
 
224
  def plot_drawdown_analysis(start_date, end_date, assets):
225
+ df = get_processed_data().loc[start_date:end_date]
 
226
  available = [a for a in assets if a in df.columns]
227
+ if not available: return go.Figure().update_layout(monochrome_layout("Select assets to plot"))
 
 
 
 
 
228
 
229
+ fig = make_subplots(rows=2, cols=1, shared_xaxes=True, row_heights=[0.6, 0.4],
230
+ subplot_titles=('Cumulative Performance', 'Drawdown'), vertical_spacing=0.08)
231
  for asset in available:
232
  prices = df[asset].dropna()
233
+ if not prices.empty:
234
  cum = (prices / prices.iloc[0]) * 100
235
+ rolling_max = cum.expanding().max()
236
+ drawdown = ((cum - rolling_max) / rolling_max) * 100
237
  fig.add_trace(go.Scatter(x=cum.index, y=cum, mode='lines', name=asset), row=1, col=1)
238
+ fig.add_trace(go.Scatter(x=drawdown.index, y=drawdown, mode='lines',
239
+ fill='tozeroy', name=asset, showlegend=False,
240
+ line=dict(width=1)), row=2, col=1)
241
+ fig.update_layout(monochrome_layout("📉 Drawdown Analysis", height=700), legend=dict(y=1, x=1))
242
+ fig.update_yaxes(title_text="Index (Base=100)", row=1, col=1)
243
  fig.update_yaxes(title_text="Drawdown (%)", row=2, col=1)
244
  return fig
245
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
246
  def plot_sector_rotation(start_date, end_date):
247
+ df = get_processed_data().loc[start_date:end_date]
248
+ sectors = ['Technology', 'Financials', 'Healthcare', 'Consumer_Discretionary',
249
+ 'Consumer_Staples', 'Energy', 'Materials', 'Industrials', 'Utilities']
 
 
250
  available = [s for s in sectors if s in df.columns]
251
+ if not available: return go.Figure().update_layout(monochrome_layout("No Sector Data Available"))
252
+
253
+ # Use 60-day returns for momentum
254
+ momentum = df[available].pct_change(60).iloc[-1] * 100
255
+ fig = go.Figure(go.Barpolar(
256
+ r=momentum.values,
257
+ theta=[s.replace('_', ' ') for s in momentum.index],
258
+ marker_color=COLORS['primary'],
259
+ opacity=0.8
260
  ))
261
  fig.update_layout(
262
+ monochrome_layout("🎯 Sector Rotation (3M Momentum %)"),
 
263
  polar=dict(
264
+ radialaxis=dict(visible=True, gridcolor=COLORS['grid']),
265
  angularaxis=dict(gridcolor=COLORS['grid'])
266
  )
267
  )
268
  return fig
269
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
270
  # ======================
271
+ # GRADIO UI DEFINITION
272
  # ======================
 
273
  custom_css = """
274
+ body, .gradio-container { font-family: 'Inter', sans-serif; background-color: #F8F9F9 !important; }
275
+ h1 { color: #17202A; text-align: center; font-size: 2.5em !important; }
276
+ h3 { color: #566573; text-align: center; font-weight: 500; }
277
+ .gradio-plot { box-shadow: 0 4px 6px rgba(0,0,0,0.05); border-radius: 8px !important; }
278
+ .tabs > .tab-nav > button { border-radius: 6px 6px 0 0 !important; background-color: #EAECEE !important; }
279
+ .tabs > .tab-nav > button.selected { background-color: #FFFFFF !important; border-bottom: 2px solid #2E4053; }
280
+ button.primary { background: #2E4053 !important; color: white !important; border-radius: 6px !important; }
281
+ .gradio-accordion { background-color: #FFFFFF; border: 1px solid #EAECEE !important; border-radius: 8px !important; }
282
+ footer { display: none !important }
283
  """
284
 
 
285
  COMMON_TICKERS = [
286
+ 'SP500', 'NASDAQ', 'VIX', 'Gold', 'Oil', 'TLT', 'HYG', 'DXY', 'T10Y2Y',
287
+ 'CPIAUCSL', 'ITA', 'MTUM', 'VTV', 'QUAL', 'IJR',
288
+ 'Technology', 'Financials', 'Energy', 'Healthcare', 'Utilities']
289
+
290
+ with gr.Blocks(title="Monochrome Macro Intelligence", css=custom_css, theme=gr.themes.Soft()) as demo:
291
+ gr.Markdown("# Monochrome Macro Intelligence\n### A Hedge Fund-Grade Dashboard for Geo-Macro & Factor Analysis")
292
+
293
+ with gr.Tabs() as tabs:
294
+ # --- TAB 1: GLOBAL MACRO DASHBOARD ---
295
+ with gr.Tab("🌐 Global Macro Dashboard", id=0):
296
+ with gr.Accordion("📅 Date Range Settings", open=False):
297
+ with gr.Row():
298
+ start_date_1 = gr.Textbox("2023-01-01", label="Start Date")
299
+ end_date_1 = gr.Textbox(datetime.today().strftime('%Y-%m-%d'), label="End Date")
300
+ update_btn_1 = gr.Button("🔄 Generate Dashboard", variant="primary")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
301
  with gr.Row():
302
+ with gr.Column(scale=2):
303
+ plot1 = gr.Plot() # Thematic Regime
304
+ plot2 = gr.Plot() # Macro Dashboard
305
+ with gr.Column(scale=1):
306
+ plot3 = gr.Plot() # Geopolitical Risk
307
+
308
+ update_btn_1.click(
309
+ fn=plot_thematic_regime, inputs=[start_date_1, end_date_1], outputs=plot1
310
+ ).then(
311
+ fn=plot_macro_dashboard, inputs=[start_date_1, end_date_1], outputs=plot2
312
+ ).then(
313
+ fn=plot_geopolitical_risk, inputs=[start_date_1, end_date_1], outputs=plot3
314
+ )
315
+
316
+ # --- TAB 2: ASSET DEEP DIVE ---
317
+ with gr.Tab("🔬 Asset Deep Dive", id=1):
318
+ with gr.Accordion("🔬 Analysis Configuration", open=True):
319
+ with gr.Row():
320
+ start_date_2 = gr.Textbox("2023-01-01", label="Start Date")
321
+ end_date_2 = gr.Textbox(datetime.today().strftime('%Y-%m-%d'), label="End Date")
322
+ assets = gr.Dropdown(
323
+ COMMON_TICKERS, value=['SP500', 'Gold', 'TLT', 'VIX'],
324
+ multiselect=True, label="Select Assets for Analysis"
325
+ )
326
+ update_btn_2 = gr.Button("🔬 Run Deep Dive Analysis", variant="primary")
327
  with gr.Row():
328
+ plot4 = gr.Plot() # Performance
329
+ plot5 = gr.Plot() # Correlation
330
+ plot6 = gr.Plot() # Drawdown
331
+
332
+ update_btn_2.click(
333
+ fn=plot_multi_asset_performance, inputs=[start_date_2, end_date_2, assets], outputs=plot4
334
+ ).then(
335
+ fn=plot_correlation_heatmap, inputs=[start_date_2, end_date_2, assets], outputs=plot5
336
+ ).then(
337
+ fn=plot_drawdown_analysis, inputs=[start_date_2, end_date_2, assets], outputs=plot6
338
+ )
339
+
340
+ # --- TAB 3: FACTOR & ROTATIONAL ANALYSIS ---
341
+ with gr.Tab("🔭 Factor & Rotational Analysis", id=2):
342
+ with gr.Accordion("📅 Date Range Settings", open=False):
343
+ with gr.Row():
344
+ start_date_3 = gr.Textbox("2023-01-01", label="Start Date")
345
+ end_date_3 = gr.Textbox(datetime.today().strftime('%Y-%m-%d'), label="End Date")
346
+ update_btn_3 = gr.Button("🔄 Generate Analysis", variant="primary")
347
  with gr.Row():
348
+ plot7 = gr.Plot() # Factor Regime
349
+ plot8 = gr.Plot() # Sector Rotation
350
+
351
+ update_btn_3.click(
352
+ fn=plot_factor_regime, inputs=[start_date_3, end_date_3], outputs=plot7
353
+ ).then(
354
+ fn=plot_sector_rotation, inputs=[start_date_3, end_date_3], outputs=plot8
355
+ )
356
+
357
+ # Initial load trigger
358
+ demo.load(
359
+ fn=plot_thematic_regime, inputs=[start_date_1, end_date_1], outputs=plot1
360
+ ).then(
361
+ fn=plot_macro_dashboard, inputs=[start_date_1, end_date_1], outputs=plot2
362
+ ).then(
363
+ fn=plot_geopolitical_risk, inputs=[start_date_1, end_date_1], outputs=plot3
364
+ )
365
 
366
  if __name__ == "__main__":
367
+ demo.launch(debug=True)