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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +99 -328
app.py CHANGED
@@ -2,18 +2,16 @@
2
  import gradio as gr
3
  import pandas as pd
4
  import numpy as np
5
- import plotly.express as px
6
  import plotly.graph_objects as go
7
  from plotly.subplots import make_subplots
8
  from datetime import datetime, timedelta
9
  import warnings
10
- from functools import lru_cache
11
  import os
12
 
13
  warnings.filterwarnings('ignore')
14
 
15
- # Import your data engine
16
- from geo_macro import UnifiedMarketDataDownloader, FRED_API_KEY
17
 
18
  # ======================
19
  # CONFIGURATION
@@ -22,35 +20,36 @@ from geo_macro import UnifiedMarketDataDownloader, FRED_API_KEY
22
  DATA_FILE = 'unified_market_data.csv'
23
  CACHE_HOURS = 24
24
 
25
- # Color scheme for modern dark theme
26
  COLORS = {
27
  'primary': '#00D9FF',
28
  'secondary': '#FF6B9D',
29
- 'accent': '#C0FF00',
30
  'warning': '#FFB800',
31
  'danger': '#FF3864',
32
  'success': '#00FF88',
33
  'bg_dark': '#0A0E27',
34
- 'bg_card': '#151932'
 
35
  }
36
 
 
 
 
 
 
37
  # ======================
38
- # DATA LOADING & CACHING
39
  # ======================
40
 
41
- @lru_cache(maxsize=1)
42
- def load_or_download_data(force_refresh=False):
43
- """Load data from cache or download fresh data"""
44
-
45
- # Check if cache exists and is fresh
46
- if os.path.exists(DATA_FILE) and not force_refresh:
47
  file_time = datetime.fromtimestamp(os.path.getmtime(DATA_FILE))
48
  if datetime.now() - file_time < timedelta(hours=CACHE_HOURS):
49
  print(f"📦 Loading cached data from {DATA_FILE}")
50
- df = pd.read_csv(DATA_FILE, index_col=0, parse_dates=True)
51
- return df
52
 
53
- # Download fresh data
54
  print("🔄 Downloading fresh market data...")
55
  downloader = UnifiedMarketDataDownloader(fred_api_key=FRED_API_KEY)
56
  df = downloader.download_all_data(start_date='2018-01-01')
@@ -63,7 +62,6 @@ def load_or_download_data(force_refresh=False):
63
  # ======================
64
 
65
  def add_thematic_features(df):
66
- """Add thematic momentum and z-scores"""
67
  THEMES = {
68
  "AI & Datacenters": ["Technology", "SMH", "SKYY", "BOTZ", "Cloud_Computing"],
69
  "Defense & Security": ["ITA", "XAR", "HACK", "Aerospace_Defense", "Defense_Stocks"],
@@ -81,96 +79,39 @@ def add_thematic_features(df):
81
  for name, assets in THEMES.items():
82
  available = [a for a in assets if a in df.columns]
83
  if available:
84
- # Equal-weight momentum (60-day)
85
  returns = df[available].pct_change()
86
  mom = returns.mean(axis=1).rolling(60, min_periods=30).sum()
87
- df[f"{name}_Momentum"] = mom
88
-
89
- # Z-score over 2 years
90
  mean = mom.rolling(500, min_periods=100).mean()
91
  std = mom.rolling(500, min_periods=100).std()
92
  df[f"{name}_Z"] = (mom - mean) / std
93
  else:
94
  df[f"{name}_Z"] = np.nan
95
-
96
  return df
97
 
98
- def calculate_portfolio_metrics(df, assets, lookback=252):
99
- """Calculate Sharpe, volatility, and drawdown for a portfolio"""
100
- available = [a for a in assets if a in df.columns]
101
- if not available:
102
- return pd.DataFrame()
103
-
104
- returns = df[available].pct_change().mean(axis=1)
105
-
106
- metrics = pd.DataFrame(index=df.index)
107
- metrics['Returns'] = returns
108
- metrics['Cumulative'] = (1 + returns).cumprod()
109
- metrics['Rolling_Vol'] = returns.rolling(lookback).std() * np.sqrt(252)
110
- metrics['Rolling_Sharpe'] = (returns.rolling(lookback).mean() * 252) / metrics['Rolling_Vol']
111
-
112
- # Drawdown
113
- cum_max = metrics['Cumulative'].expanding().max()
114
- metrics['Drawdown'] = (metrics['Cumulative'] - cum_max) / cum_max
115
-
116
- return metrics
117
-
118
  def get_processed_data():
119
- """Get data with all features"""
120
  df = load_or_download_data()
121
  return add_thematic_features(df)
122
 
123
  # ======================
124
- # ANALYSIS FUNCTIONS
125
  # ======================
126
 
127
- def analyze_regime_strength(df):
128
- """Analyze current regime strength"""
129
- z_cols = [col for col in df.columns if col.endswith('_Z')]
130
- if not z_cols:
131
- return pd.DataFrame()
132
-
133
- latest = df[z_cols].iloc[-1].dropna()
134
- prev_week = df[z_cols].iloc[-5].dropna() if len(df) > 5 else latest
135
-
136
- analysis = pd.DataFrame({
137
- 'Current': latest,
138
- 'Week_Ago': prev_week,
139
- 'Change': latest - prev_week,
140
- 'Strength': latest.apply(lambda x: 'Strong' if abs(x) > 1.5 else 'Moderate' if abs(x) > 0.5 else 'Weak'),
141
- 'Direction': latest.apply(lambda x: 'Bullish' if x > 0 else 'Bearish')
142
- })
143
-
144
- analysis.index = [col.replace('_Z', '').replace('_', ' ') for col in analysis.index]
145
- return analysis.sort_values('Current', ascending=False)
146
-
147
- def calculate_correlation_matrix(df, assets, window=60):
148
- """Calculate rolling correlation matrix"""
149
- available = [a for a in assets if a in df.columns]
150
- if len(available) < 2:
151
- return pd.DataFrame()
152
-
153
- returns = df[available].pct_change()
154
- corr = returns.rolling(window).corr()
155
-
156
- return corr
157
-
158
- # ======================
159
- # PLOT FUNCTIONS
160
- # ======================
161
-
162
- def create_modern_theme():
163
- """Create modern plotly theme"""
164
  return dict(
165
  plot_bgcolor=COLORS['bg_dark'],
166
  paper_bgcolor=COLORS['bg_card'],
167
- font=dict(color='white', family='Arial, sans-serif'),
168
- xaxis=dict(gridcolor='#2a2e45', showgrid=True),
169
- yaxis=dict(gridcolor='#2a2e45', showgrid=True),
 
 
170
  )
171
 
 
 
 
 
172
  def plot_regime_dashboard(start_date, end_date):
173
- """Enhanced regime heatmap with annotations"""
174
  df = get_processed_data()
175
  df = df[(df.index >= pd.to_datetime(start_date)) & (df.index <= pd.to_datetime(end_date))]
176
 
@@ -181,7 +122,7 @@ def plot_regime_dashboard(start_date, end_date):
181
  clean_names = [col.replace('_Z', '').replace('_', ' ') for col in z_cols]
182
  heatmap_data = df[z_cols].fillna(0)
183
 
184
- fig = go.Figure(data=go.Heatmap(
185
  z=heatmap_data.T.values,
186
  x=heatmap_data.index,
187
  y=clean_names,
@@ -189,29 +130,21 @@ def plot_regime_dashboard(start_date, end_date):
189
  zmid=0,
190
  zmin=-3,
191
  zmax=3,
192
- colorbar=dict(title="Z-Score", titleside='right')
193
  ))
194
-
195
- fig.update_layout(
196
- **create_modern_theme(),
197
- title=dict(text="🌍 Thematic Regime Heatmap", font=dict(size=24)),
198
- height=600,
199
- xaxis_title="Date",
200
- yaxis_title="Themes"
201
- )
202
-
203
  return fig
204
 
205
  def plot_thematic_pulse(start_date, end_date):
206
- """Current thematic strength bar chart"""
207
  df = get_processed_data()
208
  df = df[(df.index >= pd.to_datetime(start_date)) & (df.index <= pd.to_datetime(end_date))]
209
 
210
- z_cols = [f"{name}_Z" for name in [
211
  "AI & Datacenters", "Defense & Security", "Nuclear Renaissance",
212
  "China Stress", "Commodity Inflation", "Gold & Safe Havens",
213
  "Early Cycle", "Late Cycle", "Credit Stress", "Liquidity Conditions"
214
- ] if f"{name}_Z" in df.columns]
 
215
 
216
  if not z_cols:
217
  return go.Figure()
@@ -220,391 +153,229 @@ def plot_thematic_pulse(start_date, end_date):
220
  clean_names = [col.replace('_Z', '').replace('_', ' ') for col in latest.index]
221
  latest.index = clean_names
222
 
223
- # Color coding based on strength
224
  colors = [
225
  COLORS['danger'] if x < -1.5 else
226
  COLORS['warning'] if x < -0.5 else
227
  COLORS['success'] if x > 1.5 else
228
  COLORS['primary'] if x > 0.5 else
229
- '#444444'
230
  for x in latest
231
  ]
232
 
233
  fig = go.Figure(go.Bar(
234
- x=latest.values,
235
- y=latest.index,
236
- orientation='h',
237
- marker_color=colors,
238
- text=[f"{x:.2f}" for x in latest.values],
239
  textposition='outside'
240
  ))
241
-
242
- fig.update_layout(
243
- **create_modern_theme(),
244
- title=dict(text="🔥 Current Thematic Pulse", font=dict(size=24)),
245
- height=600,
246
- xaxis_title="Z-Score",
247
- yaxis_title="Themes"
248
- )
249
-
250
  return fig
251
 
252
  def plot_multi_asset_performance(start_date, end_date, assets):
253
- """Multi-asset normalized performance"""
254
  df = get_processed_data()
255
  df = df[(df.index >= pd.to_datetime(start_date)) & (df.index <= pd.to_datetime(end_date))]
256
-
257
  available = [a for a in assets if a in df.columns]
258
  if not available:
259
  return go.Figure()
260
 
261
  fig = go.Figure()
262
-
263
  for asset in available:
264
  prices = df[asset].dropna()
265
  if len(prices) > 0:
266
- normalized = (prices / prices.iloc[0]) * 100
267
- fig.add_trace(go.Scatter(
268
- x=normalized.index,
269
- y=normalized,
270
- mode='lines',
271
- name=asset,
272
- line=dict(width=2)
273
- ))
274
-
275
- fig.update_layout(
276
- **create_modern_theme(),
277
- title=dict(text="📈 Multi-Asset Performance (Normalized)", font=dict(size=24)),
278
- height=600,
279
- xaxis_title="Date",
280
- yaxis_title="Performance (Base = 100)",
281
- hovermode='x unified'
282
- )
283
 
 
284
  return fig
285
 
286
  def plot_correlation_heatmap(start_date, end_date, assets):
287
- """Correlation matrix heatmap"""
288
  df = get_processed_data()
289
  df = df[(df.index >= pd.to_datetime(start_date)) & (df.index <= pd.to_datetime(end_date))]
290
-
291
  available = [a for a in assets if a in df.columns]
292
  if len(available) < 2:
293
  return go.Figure()
294
 
295
- returns = df[available].pct_change().dropna()
296
- corr = returns.corr()
297
-
298
- fig = go.Figure(data=go.Heatmap(
299
  z=corr.values,
300
  x=corr.columns,
301
  y=corr.columns,
302
  colorscale='RdBu_r',
303
  zmid=0,
304
- zmin=-1,
305
- zmax=1,
306
  text=np.round(corr.values, 2),
307
  texttemplate='%{text}',
308
- textfont={"size": 10},
309
- colorbar=dict(title="Correlation")
310
  ))
311
-
312
- fig.update_layout(
313
- **create_modern_theme(),
314
- title=dict(text="🔗 Asset Correlation Matrix", font=dict(size=24)),
315
- height=700,
316
- width=800
317
- )
318
-
319
  return fig
320
 
321
  def plot_drawdown_analysis(start_date, end_date, assets):
322
- """Drawdown analysis for selected assets"""
323
  df = get_processed_data()
324
  df = df[(df.index >= pd.to_datetime(start_date)) & (df.index <= pd.to_datetime(end_date))]
325
-
326
  available = [a for a in assets if a in df.columns]
327
  if not available:
328
  return go.Figure()
329
 
330
  fig = make_subplots(rows=2, cols=1, shared_xaxes=True,
331
  subplot_titles=('Cumulative Performance', 'Drawdown'),
332
- vertical_spacing=0.1, row_heights=[0.6, 0.4])
333
 
334
  for asset in available:
335
  prices = df[asset].dropna()
336
  if len(prices) > 0:
337
- cum_ret = (prices / prices.iloc[0]) * 100
338
- cum_max = cum_ret.expanding().max()
339
- drawdown = ((cum_ret - cum_max) / cum_max) * 100
340
-
341
- fig.add_trace(go.Scatter(x=cum_ret.index, y=cum_ret, mode='lines', name=asset),
342
- row=1, col=1)
343
- fig.add_trace(go.Scatter(x=drawdown.index, y=drawdown, mode='lines',
344
- name=asset, showlegend=False, fill='tozeroy'),
345
- row=2, col=1)
346
-
347
- fig.update_layout(
348
- **create_modern_theme(),
349
- title=dict(text="📉 Drawdown Analysis", font=dict(size=24)),
350
- height=800,
351
- hovermode='x unified'
352
- )
353
 
 
354
  fig.update_xaxes(title_text="Date", row=2, col=1)
355
- fig.update_yaxes(title_text="Performance (%)", row=1, col=1)
356
  fig.update_yaxes(title_text="Drawdown (%)", row=2, col=1)
357
-
358
  return fig
359
 
360
  def plot_rolling_sharpe(start_date, end_date, assets, window=252):
361
- """Rolling Sharpe ratio analysis"""
362
  df = get_processed_data()
363
  df = df[(df.index >= pd.to_datetime(start_date)) & (df.index <= pd.to_datetime(end_date))]
364
-
365
  available = [a for a in assets if a in df.columns]
366
  if not available:
367
  return go.Figure()
368
 
369
  fig = go.Figure()
370
-
371
  for asset in available:
372
- returns = df[asset].pct_change().dropna()
373
- if len(returns) > window:
374
- rolling_sharpe = (returns.rolling(window).mean() * 252) / (returns.rolling(window).std() * np.sqrt(252))
375
- fig.add_trace(go.Scatter(
376
- x=rolling_sharpe.index,
377
- y=rolling_sharpe,
378
- mode='lines',
379
- name=asset
380
- ))
381
 
382
  fig.add_hline(y=0, line_dash="dash", line_color="gray")
383
- fig.add_hline(y=1, line_dash="dot", line_color=COLORS['success'], annotation_text="Sharpe=1")
384
-
385
- fig.update_layout(
386
- **create_modern_theme(),
387
- title=dict(text=f"📊 Rolling Sharpe Ratio ({window//252}Y)", font=dict(size=24)),
388
- height=600,
389
- xaxis_title="Date",
390
- yaxis_title="Sharpe Ratio",
391
- hovermode='x unified'
392
- )
393
-
394
  return fig
395
 
396
  def plot_sector_rotation(start_date, end_date):
397
- """Sector rotation spider chart"""
398
  df = get_processed_data()
399
  df = df[(df.index >= pd.to_datetime(start_date)) & (df.index <= pd.to_datetime(end_date))]
400
-
401
  sectors = ['Technology', 'Financials', 'Healthcare', 'Consumer_Discretionary',
402
  'Consumer_Staples', 'Energy', 'Materials', 'Industrials', 'Utilities',
403
  'Real_Estate', 'Communication_Services']
404
-
405
  available = [s for s in sectors if s in df.columns]
406
  if not available:
407
  return go.Figure()
408
 
409
- # Calculate 3-month momentum
410
- momentum = {}
411
- for sector in available:
412
- ret = df[sector].pct_change(60).iloc[-1] * 100
413
- momentum[sector] = ret
414
-
415
- fig = go.Figure()
416
-
417
- fig.add_trace(go.Scatterpolar(
418
  r=list(momentum.values()),
419
  theta=[s.replace('_', ' ') for s in momentum.keys()],
420
  fill='toself',
421
- name='3M Momentum',
422
  line_color=COLORS['primary']
423
  ))
424
-
425
  fig.update_layout(
426
- **create_modern_theme(),
 
427
  polar=dict(
428
- radialaxis=dict(visible=True, gridcolor='#2a2e45'),
429
- angularaxis=dict(gridcolor='#2a2e45')
430
- ),
431
- title=dict(text="🎯 Sector Rotation (3M Momentum %)", font=dict(size=24)),
432
- height=700
433
  )
434
-
435
  return fig
436
 
437
  def plot_risk_dashboard(start_date, end_date):
438
- """Risk indicators dashboard"""
439
  df = get_processed_data()
440
  df = df[(df.index >= pd.to_datetime(start_date)) & (df.index <= pd.to_datetime(end_date))]
441
-
442
  risk_assets = ['VIX', 'HYG', 'T10Y2Y', 'DXY', 'Gold']
443
  available = [a for a in risk_assets if a in df.columns]
444
-
445
  if not available:
446
  return go.Figure()
447
 
448
- fig = make_subplots(
449
- rows=len(available), cols=1,
450
- shared_xaxes=True,
451
- subplot_titles=[a.replace('_', ' ') for a in available],
452
- vertical_spacing=0.05
453
- )
454
 
455
  for i, asset in enumerate(available, 1):
456
  prices = df[asset].dropna()
457
  if len(prices) > 0:
458
- fig.add_trace(
459
- go.Scatter(x=prices.index, y=prices, mode='lines',
460
- name=asset, line=dict(color=COLORS['primary'])),
461
- row=i, col=1
462
- )
463
-
464
- fig.update_layout(
465
- **create_modern_theme(),
466
- title=dict(text="⚠️ Risk Indicators Dashboard", font=dict(size=24)),
467
- height=250 * len(available),
468
- showlegend=False,
469
- hovermode='x unified'
470
- )
471
 
 
472
  return fig
473
 
474
  # ======================
475
  # GRADIO UI
476
  # ======================
477
 
478
- # Custom CSS for modern dark theme
479
  custom_css = """
480
- .gradio-container {
481
- background: linear-gradient(135deg, #0A0E27 0%, #1a1f3a 100%) !important;
482
- }
483
- .tabs {
484
- border-radius: 10px;
485
- }
486
- button {
487
- border-radius: 8px !important;
488
- font-weight: 600 !important;
489
- }
490
  """
491
 
492
- # Get all available tickers
493
- df_sample = load_or_download_data()
494
- all_tickers = sorted([col for col in df_sample.columns if not col.endswith('_Z') and not col.endswith('_Momentum')])
 
 
 
 
495
 
496
- with gr.Blocks(title="Hedge Fund Intelligence Platform", css=custom_css, theme=gr.themes.Base()) as demo:
497
-
498
- gr.Markdown("""
499
- # 🏦 Macro-Thematic Intelligence Platform
500
- ### Professional-Grade Market Analytics & Regime Detection
501
- """)
502
 
503
  with gr.Tabs():
504
-
505
- # Regime Analysis
506
  with gr.Tab("🌍 Regime Dashboard"):
507
  with gr.Row():
508
  s1 = gr.Textbox("2023-01-01", label="Start Date")
509
  e1 = gr.Textbox(datetime.today().strftime('%Y-%m-%d'), label="End Date")
510
- gr.Button("🔄 Update Dashboard", variant="primary").click(
511
- plot_regime_dashboard, [s1, e1], gr.Plot()
512
- )
513
 
514
- # Thematic Pulse
515
  with gr.Tab("🔥 Thematic Pulse"):
516
  with gr.Row():
517
  s2 = gr.Textbox("2023-01-01", label="Start Date")
518
  e2 = gr.Textbox(datetime.today().strftime('%Y-%m-%d'), label="End Date")
519
- gr.Button("🔄 Analyze Themes", variant="primary").click(
520
- plot_thematic_pulse, [s2, e2], gr.Plot()
521
- )
522
 
523
- # Multi-Asset Performance
524
- with gr.Tab("📈 Performance Tracker"):
525
  with gr.Row():
526
  s3 = gr.Textbox("2023-01-01", label="Start Date")
527
  e3 = gr.Textbox(datetime.today().strftime('%Y-%m-%d'), label="End Date")
528
- assets1 = gr.Dropdown(
529
- all_tickers,
530
- value=['SP500', 'NASDAQ', 'Gold', 'Oil', 'TLT'],
531
- multiselect=True,
532
- label="Select Assets"
533
- )
534
- gr.Button("📊 Plot Performance", variant="primary").click(
535
- plot_multi_asset_performance, [s3, e3, assets1], gr.Plot()
536
- )
537
 
538
- # Correlation Analysis
539
- with gr.Tab("🔗 Correlation Matrix"):
540
  with gr.Row():
541
  s4 = gr.Textbox("2023-01-01", label="Start Date")
542
  e4 = gr.Textbox(datetime.today().strftime('%Y-%m-%d'), label="End Date")
543
- assets2 = gr.Dropdown(
544
- all_tickers,
545
- value=['SP500', 'Gold', 'Oil', 'DXY', 'TLT', 'VIX'],
546
- multiselect=True,
547
- label="Select Assets"
548
- )
549
- gr.Button("🔍 Calculate Correlations", variant="primary").click(
550
- plot_correlation_heatmap, [s4, e4, assets2], gr.Plot()
551
- )
552
 
553
- # Drawdown Analysis
554
- with gr.Tab("📉 Drawdown Analysis"):
555
  with gr.Row():
556
  s5 = gr.Textbox("2023-01-01", label="Start Date")
557
  e5 = gr.Textbox(datetime.today().strftime('%Y-%m-%d'), label="End Date")
558
- assets3 = gr.Dropdown(
559
- all_tickers,
560
- value=['SP500', 'NASDAQ', 'Gold', 'Oil'],
561
- multiselect=True,
562
- label="Select Assets"
563
- )
564
- gr.Button("📉 Analyze Drawdowns", variant="primary").click(
565
- plot_drawdown_analysis, [s5, e5, assets3], gr.Plot()
566
- )
567
 
568
- # Rolling Sharpe
569
  with gr.Tab("📊 Sharpe Ratio"):
570
  with gr.Row():
571
  s6 = gr.Textbox("2023-01-01", label="Start Date")
572
  e6 = gr.Textbox(datetime.today().strftime('%Y-%m-%d'), label="End Date")
573
- assets4 = gr.Dropdown(
574
- all_tickers,
575
- value=['SP500', 'Gold', 'TLT'],
576
- multiselect=True,
577
- label="Select Assets"
578
- )
579
  window = gr.Slider(60, 504, value=252, step=21, label="Rolling Window (days)")
580
- gr.Button("📈 Calculate Sharpe", variant="primary").click(
581
- plot_rolling_sharpe, [s6, e6, assets4, window], gr.Plot()
582
- )
583
 
584
- # Sector Rotation
585
- with gr.Tab("🎯 Sector Rotation"):
586
  with gr.Row():
587
  s7 = gr.Textbox("2023-01-01", label="Start Date")
588
  e7 = gr.Textbox(datetime.today().strftime('%Y-%m-%d'), label="End Date")
589
- gr.Button("🔄 Analyze Sectors", variant="primary").click(
590
- plot_sector_rotation, [s7, e7], gr.Plot()
591
- )
592
 
593
- # Risk Dashboard
594
  with gr.Tab("⚠️ Risk Monitor"):
595
  with gr.Row():
596
  s8 = gr.Textbox("2023-01-01", label="Start Date")
597
  e8 = gr.Textbox(datetime.today().strftime('%Y-%m-%d'), label="End Date")
598
- gr.Button("🚨 Load Risk Indicators", variant="primary").click(
599
- plot_risk_dashboard, [s8, e8], gr.Plot()
600
- )
601
 
602
- gr.Markdown("""
603
- ---
604
- **Data Sources:** Yahoo Finance, FRED
605
- **Refresh Rate:** 24 hours
606
- **Last Updated:** Real-time market data
607
- """)
608
 
609
  if __name__ == "__main__":
610
  demo.launch()
 
2
  import gradio as gr
3
  import pandas as pd
4
  import numpy as np
 
5
  import plotly.graph_objects as go
6
  from plotly.subplots import make_subplots
7
  from datetime import datetime, timedelta
8
  import warnings
 
9
  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
 
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')
 
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"],
 
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
 
 
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,
 
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()
 
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()