varun500 commited on
Commit
0d15e90
Β·
verified Β·
1 Parent(s): 0d6c1c9

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +438 -0
app.py ADDED
@@ -0,0 +1,438 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Sensex 30 & Dow Jones 30 β€” Real-Time Stock & News Tracker
3
+ Gradio app for Hugging Face Spaces. No API keys required.
4
+ """
5
+
6
+ import gradio as gr
7
+ import yfinance as yf
8
+ import feedparser
9
+ from urllib.parse import quote
10
+ from datetime import datetime
11
+ from concurrent.futures import ThreadPoolExecutor, as_completed
12
+ import threading
13
+ import time
14
+
15
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
16
+ # Company Definitions
17
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
18
+
19
+ SENSEX_30 = [
20
+ {"name": "Reliance Industries", "ticker": "RELIANCE.NS", "search": "Reliance Industries"},
21
+ {"name": "TCS", "ticker": "TCS.NS", "search": "TCS Tata Consultancy"},
22
+ {"name": "HDFC Bank", "ticker": "HDFCBANK.NS", "search": "HDFC Bank"},
23
+ {"name": "ICICI Bank", "ticker": "ICICIBANK.NS", "search": "ICICI Bank"},
24
+ {"name": "Infosys", "ticker": "INFY.NS", "search": "Infosys"},
25
+ {"name": "Bharti Airtel", "ticker": "BHARTIARTL.NS", "search": "Bharti Airtel"},
26
+ {"name": "State Bank of India", "ticker": "SBIN.NS", "search": "State Bank of India"},
27
+ {"name": "ITC", "ticker": "ITC.NS", "search": "ITC Limited"},
28
+ {"name": "HCL Technologies", "ticker": "HCLTECH.NS", "search": "HCL Technologies"},
29
+ {"name": "Hindustan Unilever", "ticker": "HINDUNILVR.NS", "search": "Hindustan Unilever"},
30
+ {"name": "Larsen & Toubro", "ticker": "LT.NS", "search": "Larsen Toubro"},
31
+ {"name": "Bajaj Finance", "ticker": "BAJFINANCE.NS", "search": "Bajaj Finance"},
32
+ {"name": "Kotak Mahindra Bank", "ticker": "KOTAKBANK.NS", "search": "Kotak Mahindra Bank"},
33
+ {"name": "Axis Bank", "ticker": "AXISBANK.NS", "search": "Axis Bank"},
34
+ {"name": "Maruti Suzuki", "ticker": "MARUTI.NS", "search": "Maruti Suzuki"},
35
+ {"name": "Sun Pharma", "ticker": "SUNPHARMA.NS", "search": "Sun Pharma"},
36
+ {"name": "Mahindra & Mahindra", "ticker": "M&M.NS", "search": "Mahindra Mahindra"},
37
+ {"name": "Titan Company", "ticker": "TITAN.NS", "search": "Titan Company"},
38
+ {"name": "Tata Motors", "ticker": "TATAMOTORS.NS", "search": "Tata Motors"},
39
+ {"name": "NTPC", "ticker": "NTPC.NS", "search": "NTPC"},
40
+ {"name": "Asian Paints", "ticker": "ASIANPAINT.NS", "search": "Asian Paints"},
41
+ {"name": "Bajaj Finserv", "ticker": "BAJAJFINSV.NS", "search": "Bajaj Finserv"},
42
+ {"name": "Power Grid Corp", "ticker": "POWERGRID.NS", "search": "Power Grid Corporation"},
43
+ {"name": "Wipro", "ticker": "WIPRO.NS", "search": "Wipro"},
44
+ {"name": "Nestle India", "ticker": "NESTLEIND.NS", "search": "Nestle India"},
45
+ {"name": "Tech Mahindra", "ticker": "TECHM.NS", "search": "Tech Mahindra"},
46
+ {"name": "IndusInd Bank", "ticker": "INDUSINDBK.NS", "search": "IndusInd Bank"},
47
+ {"name": "Tata Steel", "ticker": "TATASTEEL.NS", "search": "Tata Steel"},
48
+ {"name": "Adani Ports", "ticker": "ADANIPORTS.NS", "search": "Adani Ports"},
49
+ {"name": "Eternal (Zomato)", "ticker": "ETERNAL.NS", "search": "Zomato Eternal"},
50
+ ]
51
+
52
+ DOW_30 = [
53
+ {"name": "Apple", "ticker": "AAPL", "search": "Apple Inc"},
54
+ {"name": "Amazon", "ticker": "AMZN", "search": "Amazon"},
55
+ {"name": "Amgen", "ticker": "AMGN", "search": "Amgen"},
56
+ {"name": "American Express", "ticker": "AXP", "search": "American Express"},
57
+ {"name": "Boeing", "ticker": "BA", "search": "Boeing"},
58
+ {"name": "Caterpillar", "ticker": "CAT", "search": "Caterpillar"},
59
+ {"name": "Salesforce", "ticker": "CRM", "search": "Salesforce"},
60
+ {"name": "Cisco", "ticker": "CSCO", "search": "Cisco Systems"},
61
+ {"name": "Chevron", "ticker": "CVX", "search": "Chevron"},
62
+ {"name": "Disney", "ticker": "DIS", "search": "Walt Disney"},
63
+ {"name": "Goldman Sachs", "ticker": "GS", "search": "Goldman Sachs"},
64
+ {"name": "Home Depot", "ticker": "HD", "search": "Home Depot"},
65
+ {"name": "Honeywell", "ticker": "HON", "search": "Honeywell"},
66
+ {"name": "IBM", "ticker": "IBM", "search": "IBM"},
67
+ {"name": "Johnson & Johnson","ticker": "JNJ", "search": "Johnson Johnson"},
68
+ {"name": "JPMorgan Chase", "ticker": "JPM", "search": "JPMorgan Chase"},
69
+ {"name": "Coca-Cola", "ticker": "KO", "search": "Coca-Cola"},
70
+ {"name": "McDonald's", "ticker": "MCD", "search": "McDonalds"},
71
+ {"name": "3M", "ticker": "MMM", "search": "3M Company"},
72
+ {"name": "Merck", "ticker": "MRK", "search": "Merck"},
73
+ {"name": "Microsoft", "ticker": "MSFT", "search": "Microsoft"},
74
+ {"name": "Nike", "ticker": "NKE", "search": "Nike"},
75
+ {"name": "NVIDIA", "ticker": "NVDA", "search": "NVIDIA"},
76
+ {"name": "Procter & Gamble", "ticker": "PG", "search": "Procter Gamble"},
77
+ {"name": "Sherwin-Williams", "ticker": "SHW", "search": "Sherwin Williams"},
78
+ {"name": "Travelers", "ticker": "TRV", "search": "Travelers Companies"},
79
+ {"name": "UnitedHealth", "ticker": "UNH", "search": "UnitedHealth Group"},
80
+ {"name": "Visa", "ticker": "V", "search": "Visa Inc"},
81
+ {"name": "Verizon", "ticker": "VZ", "search": "Verizon"},
82
+ {"name": "Walmart", "ticker": "WMT", "search": "Walmart"},
83
+ ]
84
+
85
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
86
+ # Simple TTL Cache
87
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
88
+
89
+ _cache = {}
90
+ _cache_lock = threading.Lock()
91
+ CACHE_TTL = 120 # seconds
92
+
93
+
94
+ def cached_fetch(key, fetch_fn):
95
+ with _cache_lock:
96
+ if key in _cache:
97
+ val, ts = _cache[key]
98
+ if time.time() - ts < CACHE_TTL:
99
+ return val
100
+ result = fetch_fn()
101
+ with _cache_lock:
102
+ _cache[key] = (result, time.time())
103
+ return result
104
+
105
+
106
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
107
+ # Data Fetching
108
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
109
+
110
+ def get_stock_data(ticker: str) -> dict:
111
+ def _fetch():
112
+ try:
113
+ stock = yf.Ticker(ticker)
114
+ info = stock.fast_info
115
+ price = info.last_price or 0
116
+ prev_close = info.previous_close or price
117
+ change_pct = ((price - prev_close) / prev_close * 100) if prev_close else 0
118
+
119
+ mkt_cap = getattr(info, "market_cap", None)
120
+ if mkt_cap and mkt_cap >= 1e12:
121
+ cap_str = f"{mkt_cap/1e12:.2f}T"
122
+ elif mkt_cap and mkt_cap >= 1e9:
123
+ cap_str = f"{mkt_cap/1e9:.2f}B"
124
+ elif mkt_cap and mkt_cap >= 1e6:
125
+ cap_str = f"{mkt_cap/1e6:.0f}M"
126
+ else:
127
+ cap_str = "N/A"
128
+
129
+ currency = getattr(info, "currency", "USD")
130
+ sym = "\u20b9" if currency == "INR" else "$"
131
+
132
+ return {
133
+ "price": price,
134
+ "price_str": f"{sym}{price:,.2f}",
135
+ "change_pct": round(change_pct, 2),
136
+ "market_cap": cap_str,
137
+ "currency": sym,
138
+ }
139
+ except Exception:
140
+ return {"price": 0, "price_str": "N/A", "change_pct": 0, "market_cap": "N/A", "currency": "$"}
141
+
142
+ return cached_fetch(f"stock:{ticker}", _fetch)
143
+
144
+
145
+ def get_news(query: str, max_items: int = 3) -> list[dict]:
146
+ def _fetch():
147
+ url = f"https://news.google.com/rss/search?q={quote(query)}+stock&hl=en&gl=US&ceid=US:en"
148
+ try:
149
+ feed = feedparser.parse(url)
150
+ items = []
151
+ for entry in feed.entries[:max_items]:
152
+ pub = ""
153
+ try:
154
+ dt = datetime(*entry.published_parsed[:6])
155
+ pub = dt.strftime("%b %d, %H:%M")
156
+ except Exception:
157
+ pub = entry.get("published", "")[:20]
158
+ title = entry.title
159
+ # Google News titles often end with " - Source", keep it
160
+ items.append({"title": title[:120], "published": pub, "link": entry.link})
161
+ return items
162
+ except Exception:
163
+ return []
164
+
165
+ return cached_fetch(f"news:{query}", _fetch)
166
+
167
+
168
+ def fetch_company(co: dict) -> dict:
169
+ """Fetch stock data + news for one company."""
170
+ data = get_stock_data(co["ticker"])
171
+ news = get_news(co["search"])
172
+ return {**co, "_data": data, "_news": news}
173
+
174
+
175
+ def fetch_all_parallel(companies: list[dict]) -> list[dict]:
176
+ results = [None] * len(companies)
177
+ with ThreadPoolExecutor(max_workers=10) as pool:
178
+ future_to_idx = {pool.submit(fetch_company, co): i for i, co in enumerate(companies)}
179
+ for future in as_completed(future_to_idx):
180
+ idx = future_to_idx[future]
181
+ try:
182
+ results[idx] = future.result()
183
+ except Exception:
184
+ results[idx] = {**companies[idx], "_data": {"price": 0, "price_str": "N/A", "change_pct": 0, "market_cap": "N/A", "currency": "$"}, "_news": []}
185
+ return results
186
+
187
+
188
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
189
+ # HTML Rendering
190
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
191
+
192
+ CSS = """
193
+ <style>
194
+ .tracker-table {
195
+ width: 100%;
196
+ border-collapse: collapse;
197
+ font-family: 'Inter', 'Segoe UI', system-ui, sans-serif;
198
+ font-size: 14px;
199
+ margin-bottom: 20px;
200
+ }
201
+ .tracker-table th {
202
+ background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
203
+ color: #00d4ff;
204
+ padding: 12px 16px;
205
+ text-align: left;
206
+ font-weight: 600;
207
+ font-size: 13px;
208
+ text-transform: uppercase;
209
+ letter-spacing: 0.5px;
210
+ border-bottom: 2px solid #0f3460;
211
+ }
212
+ .tracker-table th:nth-child(n+4) { text-align: right; }
213
+ .tracker-table td {
214
+ padding: 10px 16px;
215
+ border-bottom: 1px solid #e8e8e8;
216
+ }
217
+ .tracker-table td:nth-child(n+4) { text-align: right; font-family: 'JetBrains Mono', 'Fira Code', monospace; }
218
+ .tracker-table tr:hover { background: #f0f9ff; }
219
+ .tracker-table tr:nth-child(even) { background: #fafbfc; }
220
+ .tracker-table tr:nth-child(even):hover { background: #f0f9ff; }
221
+ .ticker-cell { color: #0066cc; font-weight: 600; }
222
+ .company-cell { font-weight: 500; }
223
+ .change-pos { color: #00a651; font-weight: 700; }
224
+ .change-neg { color: #e63946; font-weight: 700; }
225
+ .change-flat { color: #888; }
226
+ .section-title {
227
+ font-size: 22px;
228
+ font-weight: 700;
229
+ margin: 20px 0 12px 0;
230
+ padding: 10px 16px;
231
+ background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
232
+ color: white;
233
+ border-radius: 8px;
234
+ }
235
+ .news-grid {
236
+ display: grid;
237
+ grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
238
+ gap: 16px;
239
+ margin-bottom: 24px;
240
+ }
241
+ .news-card {
242
+ background: #ffffff;
243
+ border: 1px solid #e0e7ef;
244
+ border-radius: 10px;
245
+ padding: 16px;
246
+ transition: box-shadow 0.2s;
247
+ }
248
+ .news-card:hover { box-shadow: 0 4px 16px rgba(0,0,0,0.08); }
249
+ .news-card-title {
250
+ font-size: 15px;
251
+ font-weight: 700;
252
+ margin-bottom: 8px;
253
+ color: #1a1a2e;
254
+ }
255
+ .news-item {
256
+ padding: 8px 0;
257
+ border-bottom: 1px solid #f0f0f0;
258
+ }
259
+ .news-item:last-child { border-bottom: none; }
260
+ .news-item a {
261
+ color: #1a1a2e;
262
+ text-decoration: none;
263
+ font-size: 13px;
264
+ line-height: 1.4;
265
+ display: block;
266
+ }
267
+ .news-item a:hover { color: #0066cc; }
268
+ .news-date {
269
+ font-size: 11px;
270
+ color: #999;
271
+ margin-top: 3px;
272
+ }
273
+ .update-bar {
274
+ text-align: center;
275
+ color: #888;
276
+ font-size: 13px;
277
+ padding: 8px;
278
+ margin-bottom: 16px;
279
+ background: #f8f9fa;
280
+ border-radius: 6px;
281
+ }
282
+ </style>
283
+ """
284
+
285
+
286
+ def change_html(pct: float) -> str:
287
+ if pct > 0:
288
+ return f'<span class="change-pos">+{pct:.2f}%</span>'
289
+ elif pct < 0:
290
+ return f'<span class="change-neg">{pct:.2f}%</span>'
291
+ return f'<span class="change-flat">{pct:.2f}%</span>'
292
+
293
+
294
+ def build_stock_table_html(companies: list[dict], title: str) -> str:
295
+ rows = ""
296
+ for i, co in enumerate(companies, 1):
297
+ d = co["_data"]
298
+ rows += f"""<tr>
299
+ <td>{i}</td>
300
+ <td class="company-cell">{co['name']}</td>
301
+ <td class="ticker-cell">{co['ticker'].replace('.NS','')}</td>
302
+ <td>{d['price_str']}</td>
303
+ <td>{change_html(d['change_pct'])}</td>
304
+ <td>{d['market_cap']}</td>
305
+ </tr>"""
306
+
307
+ return f"""
308
+ <div class="section-title">{title}</div>
309
+ <table class="tracker-table">
310
+ <thead>
311
+ <tr>
312
+ <th>#</th>
313
+ <th>Company</th>
314
+ <th>Ticker</th>
315
+ <th>Price</th>
316
+ <th>Change</th>
317
+ <th>Mkt Cap</th>
318
+ </tr>
319
+ </thead>
320
+ <tbody>{rows}</tbody>
321
+ </table>
322
+ """
323
+
324
+
325
+ def build_news_html(companies: list[dict], title: str) -> str:
326
+ cards = ""
327
+ for co in companies:
328
+ news = co.get("_news", [])
329
+ if not news:
330
+ items_html = '<div class="news-item"><span style="color:#999;">No news available.</span></div>'
331
+ else:
332
+ items_html = ""
333
+ for n in news:
334
+ items_html += f"""<div class="news-item">
335
+ <a href="{n['link']}" target="_blank">{n['title']}</a>
336
+ <div class="news-date">{n['published']}</div>
337
+ </div>"""
338
+
339
+ cards += f"""<div class="news-card">
340
+ <div class="news-card-title">{co['name']}</div>
341
+ {items_html}
342
+ </div>"""
343
+
344
+ return f"""
345
+ <div class="section-title">{title}</div>
346
+ <div class="news-grid">{cards}</div>
347
+ """
348
+
349
+
350
+ # ━━━━━━━━━━━━━���━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
351
+ # Gradio Callbacks
352
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
353
+
354
+ def refresh_sensex():
355
+ companies = fetch_all_parallel(SENSEX_30)
356
+ now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
357
+ table_html = build_stock_table_html(companies, "\U0001f1ee\U0001f1f3 BSE Sensex 30")
358
+ news_html = build_news_html(companies, "\U0001f1ee\U0001f1f3 Sensex 30 β€” Latest News")
359
+ update_bar = f'<div class="update-bar">Last updated: {now} &nbsp;|&nbsp; Data via Yahoo Finance &nbsp;|&nbsp; News via Google News RSS</div>'
360
+ return CSS + update_bar + table_html + news_html
361
+
362
+
363
+ def refresh_dow():
364
+ companies = fetch_all_parallel(DOW_30)
365
+ now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
366
+ table_html = build_stock_table_html(companies, "\U0001f1fa\U0001f1f8 Dow Jones 30")
367
+ news_html = build_news_html(companies, "\U0001f1fa\U0001f1f8 Dow 30 β€” Latest News")
368
+ update_bar = f'<div class="update-bar">Last updated: {now} &nbsp;|&nbsp; Data via Yahoo Finance &nbsp;|&nbsp; News via Google News RSS</div>'
369
+ return CSS + update_bar + table_html + news_html
370
+
371
+
372
+ def refresh_all():
373
+ return refresh_sensex(), refresh_dow()
374
+
375
+
376
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
377
+ # Gradio UI
378
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
379
+
380
+ HEADER_HTML = """
381
+ <div style="text-align:center; padding: 20px 0 10px 0;">
382
+ <h1 style="margin:0; font-size:32px; background: linear-gradient(90deg, #ff9933, #ffffff, #138808, #0052b5, #bf0a30, #ffffff); -webkit-background-clip: text; -webkit-text-fill-color: transparent;">
383
+ Sensex 30 & Dow Jones 30 Tracker
384
+ </h1>
385
+ <p style="color:#666; margin:8px 0 0 0; font-size:15px;">
386
+ Real-time stock prices and news &mdash; Powered by Yahoo Finance & Google News
387
+ </p>
388
+ </div>
389
+ """
390
+
391
+ LOADING_HTML = """
392
+ <div style="text-align:center; padding:60px; color:#888;">
393
+ <p style="font-size:18px;">Click <b>Refresh Data</b> or wait for auto-load...</p>
394
+ </div>
395
+ """
396
+
397
+ with gr.Blocks(
398
+ title="Sensex 30 & Dow 30 Tracker",
399
+ theme=gr.themes.Soft(primary_hue="blue", neutral_hue="slate"),
400
+ css=".gradio-container { max-width: 1400px !important; }",
401
+ ) as demo:
402
+
403
+ gr.HTML(HEADER_HTML)
404
+
405
+ with gr.Row():
406
+ refresh_btn = gr.Button(
407
+ "Refresh Data",
408
+ variant="primary",
409
+ size="lg",
410
+ scale=1,
411
+ )
412
+ gr.HTML('<div style="flex:3"></div>')
413
+
414
+ with gr.Tabs():
415
+ with gr.Tab("\U0001f1ee\U0001f1f3 Sensex 30"):
416
+ sensex_output = gr.HTML(value=LOADING_HTML)
417
+ with gr.Tab("\U0001f1fa\U0001f1f8 Dow Jones 30"):
418
+ dow_output = gr.HTML(value=LOADING_HTML)
419
+
420
+ # Refresh button fetches both tabs
421
+ refresh_btn.click(
422
+ fn=refresh_all,
423
+ inputs=[],
424
+ outputs=[sensex_output, dow_output],
425
+ show_progress="full",
426
+ )
427
+
428
+ # Auto-load on page open
429
+ demo.load(
430
+ fn=refresh_all,
431
+ inputs=[],
432
+ outputs=[sensex_output, dow_output],
433
+ show_progress="full",
434
+ )
435
+
436
+
437
+ if __name__ == "__main__":
438
+ demo.launch()