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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +349 -303
app.py CHANGED
@@ -1,367 +1,413 @@
1
- # app.py
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
- # 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)
 
 
1
  import gradio as gr
2
  import pandas as pd
3
  import numpy as np
4
  import plotly.graph_objects as go
5
  from plotly.subplots import make_subplots
6
+ from datetime import datetime
7
  import warnings
8
  import os
9
 
10
  warnings.filterwarnings('ignore')
11
 
 
12
  from geo_macro import UnifiedMarketDataDownloader
13
 
14
+ # Configuration
15
+ FRED_API_KEY = os.getenv("FRED_API_KEY", "23f3511b0ca43918ccd503ef64cb844e")
 
 
 
16
 
17
+ # Professional color palette
18
  COLORS = {
19
+ 'primary': '#1a1a1a',
20
+ 'secondary': '#4a4a4a',
21
+ 'accent': '#0066cc',
22
+ 'grid': '#e5e5e5',
23
+ 'bg': '#ffffff',
24
+ 'green': '#00a86b',
25
+ 'red': '#d32f2f',
 
 
26
  }
27
 
28
+ # Cache data globally
29
+ _cached_data = None
30
+
31
+ def load_data():
32
+ """Load and cache market data"""
33
+ global _cached_data
34
+ if _cached_data is None:
35
+ downloader = UnifiedMarketDataDownloader(fred_api_key=FRED_API_KEY)
36
+ _cached_data = downloader.download_all_data(start_date='2020-01-01')
37
+ return _cached_data
38
+
39
+ def calculate_momentum_zscore(series, window=60):
40
+ """Calculate momentum z-score"""
41
+ returns = series.pct_change(window)
42
+ mean = returns.rolling(252).mean()
43
+ std = returns.rolling(252).std()
44
+ return (returns - mean) / std
45
+
46
+ def create_layout(title, height=450):
47
+ """Standard plot layout"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  return go.Layout(
49
+ title=dict(text=title, font=dict(size=16, color=COLORS['primary']), x=0.5, xanchor='center'),
50
+ plot_bgcolor=COLORS['bg'],
51
+ paper_bgcolor=COLORS['bg'],
52
+ font=dict(color=COLORS['secondary'], size=11),
53
+ xaxis=dict(gridcolor=COLORS['grid'], showline=True, linecolor=COLORS['grid']),
54
+ yaxis=dict(gridcolor=COLORS['grid'], showline=True, linecolor=COLORS['grid']),
55
  hovermode='x unified',
56
+ height=height,
57
+ margin=dict(l=60, r=40, t=60, b=40),
58
  )
59
 
60
+ # === PLOT FUNCTIONS ===
61
+
62
+ def plot_geopolitical_risk(start_date, end_date):
63
+ """Geopolitical risk composite index"""
64
+ df = load_data().loc[start_date:end_date]
65
+
66
+ # Composite: Defense, Oil, Gold, VIX
67
+ components = ['Defense', 'Oil', 'Gold', 'VIX']
68
+ available = [c for c in components if c in df.columns]
69
+
70
+ if len(available) < 2:
71
+ return go.Figure().update_layout(create_layout("Insufficient Data"))
72
+
73
+ # Normalize each component
74
+ normalized = df[available].apply(lambda x: (x / x.iloc[0]) - 1)
75
+ risk_index = normalized.mean(axis=1)
76
+
77
+ # Calculate z-score
78
+ zscore = (risk_index - risk_index.rolling(252).mean()) / risk_index.rolling(252).std()
79
+
80
+ fig = go.Figure()
81
+
82
+ # Add risk index
83
+ fig.add_trace(go.Scatter(
84
+ x=zscore.index,
85
+ y=zscore,
86
+ fill='tozeroy',
87
+ line=dict(color=COLORS['accent'], width=2),
88
+ name='Risk Index'
89
  ))
90
+
91
+ # Add threshold lines
92
+ fig.add_hline(y=0, line_dash="dash", line_color=COLORS['secondary'], line_width=1)
93
+ fig.add_hline(y=2, line_dash="dot", line_color=COLORS['red'], line_width=1)
94
+ fig.add_hline(y=-2, line_dash="dot", line_color=COLORS['green'], line_width=1)
95
+
96
+ fig.update_layout(create_layout("Geopolitical Risk Index (Z-Score)"))
97
+ fig.update_yaxes(title="Standard Deviations from Mean")
98
+
99
  return fig
100
 
101
+ def plot_macro_indicators(start_date, end_date):
102
+ """Key macro indicators dashboard"""
103
+ df = load_data().loc[start_date:end_date]
104
+
105
+ indicators = {
106
+ 'CPI': ('Inflation (YoY %)', lambda x: x.pct_change(252) * 100),
107
+ 'Yield_Curve': ('Yield Curve (10Y-2Y)', lambda x: x),
108
+ 'VIX': ('Market Volatility', lambda x: x),
109
+ 'USD_Index': ('Dollar Strength', lambda x: x),
110
+ }
111
+
 
 
 
 
 
 
 
112
  available = {k: v for k, v in indicators.items() if k in df.columns}
113
+
114
+ if not available:
115
+ return go.Figure().update_layout(create_layout("No Data Available"))
116
+
117
+ n_plots = len(available)
118
+ fig = make_subplots(
119
+ rows=n_plots, cols=1,
120
+ shared_xaxes=True,
121
+ vertical_spacing=0.05,
122
+ subplot_titles=[v[0] for v in available.values()]
123
+ )
124
+
125
+ for i, (col, (title, transform)) in enumerate(available.items(), 1):
126
+ series = transform(df[col]).dropna()
127
+
128
+ # Color based on last value
129
+ color = COLORS['red'] if series.iloc[-1] > series.mean() else COLORS['green']
130
+ if col == 'Yield_Curve':
131
+ color = COLORS['red'] if series.iloc[-1] < 0 else COLORS['green']
132
+
133
+ fig.add_trace(
134
+ go.Scatter(
135
+ x=series.index,
136
+ y=series,
137
+ line=dict(color=color, width=2),
138
+ showlegend=False
139
+ ),
140
+ row=i, col=1
141
+ )
142
+
143
+ # Add mean line
144
+ mean_val = series.mean()
145
+ fig.add_hline(
146
+ y=mean_val,
147
+ line_dash="dash",
148
+ line_color=COLORS['secondary'],
149
+ line_width=1,
150
+ row=i, col=1
151
+ )
152
+
153
+ fig.update_layout(
154
+ create_layout("Macroeconomic Indicators", height=150 * n_plots),
155
+ showlegend=False
156
+ )
157
+
158
  return fig
159
 
160
+ def plot_safe_haven_flows(start_date, end_date):
161
+ """Safe haven vs risk asset flows"""
162
+ df = load_data().loc[start_date:end_date]
163
+
164
+ safe_havens = ['Gold', 'US_10Y', 'CHF']
165
+ risk_assets = ['US_Equity', 'Emerging_Markets', 'High_Yield']
166
+
167
+ safe_available = [c for c in safe_havens if c in df.columns]
168
+ risk_available = [c for c in risk_assets if c in df.columns]
169
+
170
+ if not safe_available or not risk_available:
171
+ return go.Figure().update_layout(create_layout("Insufficient Data"))
172
+
173
+ # Normalize to 100
174
+ safe_norm = df[safe_available].apply(lambda x: x / x.iloc[0] * 100).mean(axis=1)
175
+ risk_norm = df[risk_available].apply(lambda x: x / x.iloc[0] * 100).mean(axis=1)
176
+
177
+ fig = go.Figure()
178
+
179
+ fig.add_trace(go.Scatter(
180
+ x=safe_norm.index,
181
+ y=safe_norm,
182
+ name='Safe Havens',
183
+ line=dict(color=COLORS['green'], width=2)
184
+ ))
185
+
186
+ fig.add_trace(go.Scatter(
187
+ x=risk_norm.index,
188
+ y=risk_norm,
189
+ name='Risk Assets',
190
+ line=dict(color=COLORS['red'], width=2)
191
+ ))
192
+
193
+ fig.update_layout(create_layout("Safe Haven vs Risk Asset Performance"))
194
+ fig.update_yaxes(title="Indexed to 100")
195
+
196
  return fig
197
 
198
+ def plot_relative_strength(start_date, end_date):
199
+ """Regional equity performance"""
200
+ df = load_data().loc[start_date:end_date]
201
+
202
+ regions = ['US_Equity', 'Europe', 'China', 'Emerging_Markets']
203
+ available = [r for r in regions if r in df.columns]
204
+
205
+ if len(available) < 2:
206
+ return go.Figure().update_layout(create_layout("Insufficient Data"))
207
+
208
+ # Calculate 3-month momentum
209
+ momentum = df[available].pct_change(60).iloc[-1] * 100
210
+ momentum = momentum.sort_values(ascending=False)
211
+
212
+ colors = [COLORS['green'] if x > 0 else COLORS['red'] for x in momentum.values]
213
+
214
+ fig = go.Figure(go.Bar(
215
+ x=momentum.values,
216
+ y=[name.replace('_', ' ') for name in momentum.index],
217
+ orientation='h',
218
+ marker_color=colors,
219
+ text=[f"{x:.1f}%" for x in momentum.values],
220
+ textposition='auto',
221
+ ))
222
+
223
+ fig.add_vline(x=0, line_color=COLORS['secondary'], line_width=1)
224
+
225
+ fig.update_layout(create_layout("Regional Equity Strength (3M Return)"))
226
+ fig.update_xaxes(title="Return (%)")
227
+
228
  return fig
229
 
230
+ def plot_correlation_matrix(start_date, end_date, assets):
231
+ """Correlation heatmap for selected assets"""
232
+ df = load_data().loc[start_date:end_date]
233
+
234
  available = [a for a in assets if a in df.columns]
235
+ if len(available) < 2:
236
+ return go.Figure().update_layout(create_layout("Select at least 2 assets"))
237
+
238
  corr = df[available].pct_change().corr()
239
+
240
  fig = go.Figure(go.Heatmap(
241
+ z=corr.values,
242
+ x=[c.replace('_', ' ') for c in corr.columns],
243
+ y=[c.replace('_', ' ') for c in corr.columns],
244
+ colorscale='RdBu_r',
245
+ zmid=0,
246
+ zmin=-1,
247
+ zmax=1,
248
+ text=np.round(corr.values, 2),
249
+ texttemplate='%{text}',
250
+ textfont=dict(size=10),
251
  colorbar=dict(title="Correlation")
252
  ))
253
+
254
+ fig.update_layout(create_layout("Asset Correlation Matrix", height=500))
255
+
256
  return fig
257
 
258
+ def plot_drawdown(start_date, end_date, assets):
259
+ """Drawdown analysis"""
260
+ df = load_data().loc[start_date:end_date]
261
+
262
  available = [a for a in assets if a in df.columns]
263
+ if not available:
264
+ return go.Figure().update_layout(create_layout("Select assets to analyze"))
265
+
266
+ fig = go.Figure()
267
 
 
 
268
  for asset in available:
269
  prices = df[asset].dropna()
270
+ if len(prices) > 0:
271
+ rolling_max = prices.expanding().max()
272
+ drawdown = ((prices - rolling_max) / rolling_max) * 100
273
+
274
+ fig.add_trace(go.Scatter(
275
+ x=drawdown.index,
276
+ y=drawdown,
277
+ name=asset.replace('_', ' '),
278
+ line=dict(width=2)
279
+ ))
280
+
281
+ fig.update_layout(create_layout("Drawdown Analysis"))
282
+ fig.update_yaxes(title="Drawdown (%)")
283
+
284
  return fig
285
 
286
+ # === GRADIO APP ===
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
287
 
 
 
 
288
  custom_css = """
289
+ .gradio-container {
290
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
291
+ max-width: 1400px;
292
+ margin: auto;
293
+ }
294
+ h1 {
295
+ font-size: 2.2em !important;
296
+ font-weight: 600 !important;
297
+ color: #1a1a1a !important;
298
+ text-align: center;
299
+ margin-bottom: 0.3em !important;
300
+ }
301
+ h3 {
302
+ font-size: 1.1em !important;
303
+ font-weight: 400 !important;
304
+ color: #4a4a4a !important;
305
+ text-align: center;
306
+ margin-top: 0 !important;
307
+ }
308
+ .tabs button {
309
+ font-size: 0.95em !important;
310
+ font-weight: 500 !important;
311
+ }
312
+ .tabs button.selected {
313
+ border-bottom: 2px solid #0066cc !important;
314
+ }
315
+ button.primary {
316
+ background: #0066cc !important;
317
+ font-weight: 500 !important;
318
+ }
319
  """
320
 
321
+ AVAILABLE_ASSETS = [
322
+ 'US_Equity', 'Europe', 'China', 'Emerging_Markets',
323
+ 'Gold', 'Oil', 'Copper', 'US_10Y', 'High_Yield',
324
+ 'VIX', 'USD_Index', 'Defense', 'Technology'
325
+ ]
 
 
326
 
327
+ with gr.Blocks(title="Geopolitical & Macro Intelligence", css=custom_css, theme=gr.themes.Soft()) as demo:
328
+
329
+ gr.Markdown("# Geopolitical & Macro Intelligence")
330
+ gr.Markdown("### Professional analysis of global markets and economic indicators")
331
+
332
+ with gr.Tabs():
333
+
334
+ # TAB 1: Geopolitical Overview
335
+ with gr.Tab("🌍 Geopolitical Risk"):
336
  with gr.Row():
337
+ start_1 = gr.Textbox("2023-01-01", label="Start Date", scale=1)
338
+ end_1 = gr.Textbox(datetime.today().strftime('%Y-%m-%d'), label="End Date", scale=1)
339
+ btn_1 = gr.Button("Update", variant="primary", scale=1)
 
 
340
 
341
+ with gr.Row():
342
+ plot_geo_risk = gr.Plot()
343
+ plot_safe_haven = gr.Plot()
344
+
345
+ btn_1.click(
346
+ fn=plot_geopolitical_risk,
347
+ inputs=[start_1, end_1],
348
+ outputs=plot_geo_risk
349
  ).then(
350
+ fn=plot_safe_haven_flows,
351
+ inputs=[start_1, end_1],
352
+ outputs=plot_safe_haven
353
  )
354
+
355
+ # TAB 2: Macro Dashboard
356
+ with gr.Tab("📊 Macro Indicators"):
 
 
 
 
 
 
 
 
 
357
  with gr.Row():
358
+ start_2 = gr.Textbox("2023-01-01", label="Start Date", scale=1)
359
+ end_2 = gr.Textbox(datetime.today().strftime('%Y-%m-%d'), label="End Date", scale=1)
360
+ btn_2 = gr.Button("Update", variant="primary", scale=1)
361
 
362
+ with gr.Row():
363
+ plot_macro = gr.Plot(scale=2)
364
+ plot_regional = gr.Plot(scale=1)
365
+
366
+ btn_2.click(
367
+ fn=plot_macro_indicators,
368
+ inputs=[start_2, end_2],
369
+ outputs=plot_macro
370
  ).then(
371
+ fn=plot_relative_strength,
372
+ inputs=[start_2, end_2],
373
+ outputs=plot_regional
374
  )
375
+
376
+ # TAB 3: Custom Analysis
377
+ with gr.Tab("🔍 Custom Analysis"):
 
 
 
 
 
378
  with gr.Row():
379
+ start_3 = gr.Textbox("2023-01-01", label="Start Date", scale=1)
380
+ end_3 = gr.Textbox(datetime.today().strftime('%Y-%m-%d'), label="End Date", scale=1)
381
 
382
+ assets_select = gr.Dropdown(
383
+ AVAILABLE_ASSETS,
384
+ value=['US_Equity', 'Gold', 'Oil', 'VIX'],
385
+ multiselect=True,
386
+ label="Select Assets"
387
+ )
388
+
389
+ btn_3 = gr.Button("Analyze", variant="primary")
390
+
391
+ with gr.Row():
392
+ plot_corr = gr.Plot()
393
+ plot_dd = gr.Plot()
394
+
395
+ btn_3.click(
396
+ fn=plot_correlation_matrix,
397
+ inputs=[start_3, end_3, assets_select],
398
+ outputs=plot_corr
399
  ).then(
400
+ fn=plot_drawdown,
401
+ inputs=[start_3, end_3, assets_select],
402
+ outputs=plot_dd
403
  )
404
+
405
+ # Load initial data
406
  demo.load(
407
+ fn=plot_geopolitical_risk,
408
+ inputs=[start_1, end_1],
409
+ outputs=plot_geo_risk
 
 
410
  )
411
 
412
  if __name__ == "__main__":
413
+ demo.launch()