pvyas96 commited on
Commit
5370144
·
verified ·
1 Parent(s): 741cd53

Update src/utils.py

Browse files
Files changed (1) hide show
  1. src/utils.py +151 -249
src/utils.py CHANGED
@@ -3,17 +3,112 @@ import pandas as pd
3
  import numpy as np
4
  import yfinance as yf
5
  import cvxpy as cp
 
6
  from datetime import datetime, timedelta
7
 
8
- # ============ DATA FETCHING ============
9
 
10
- @st.cache_data(ttl=86400) # Cache for 24 hours
11
- def get_nifty50_stocks():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  """
13
- Fetches the current NIFTY 50 tickers from Wikipedia to ensure the list is up to date.
14
- Falls back to a static list if scraping fails.
15
  """
16
- # Hardcoded backup list
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  backup_tickers = [
18
  "RELIANCE.NS", "TCS.NS", "HDFCBANK.NS", "INFY.NS", "ICICIBANK.NS",
19
  "HINDUNILVR.NS", "ITC.NS", "SBIN.NS", "BHARTIARTL.NS", "KOTAKBANK.NS",
@@ -26,76 +121,22 @@ def get_nifty50_stocks():
26
  "APOLLOHOSP.NS", "EICHERMOT.NS", "HEROMOTOCO.NS", "BPCL.NS", "TATACONSUM.NS",
27
  "SBILIFE.NS", "UPL.NS", "ADANIENT.NS", "HDFCLIFE.NS", "SHREECEM.NS"
28
  ]
29
-
30
- try:
31
- url = "https://en.wikipedia.org/wiki/NIFTY_50"
32
- # Extract tables from the Wikipedia page
33
- tables = pd.read_html(url)
34
-
35
- # The constituents table is usually the second one (index 1)
36
- # We check columns to be sure it's the right table
37
- df = tables[1]
38
-
39
- # Check if we got the right table by looking for 'Symbol' column
40
- if 'Symbol' not in df.columns:
41
- # Try other tables if the order changed
42
- for table in tables:
43
- if 'Symbol' in table.columns:
44
- df = table
45
- break
46
-
47
- # Extract tickers and format for Yahoo Finance (add .NS)
48
- tickers = df['Symbol'].astype(str).values.tolist()
49
- formatted_tickers = [f"{ticker}.NS" for ticker in tickers]
50
-
51
- # Basic validation: NIFTY 50 should have roughly 50 stocks
52
- if len(formatted_tickers) < 45:
53
- return backup_tickers
54
-
55
- return formatted_tickers
56
-
57
- except Exception as e:
58
- # Fallback to hardcoded list if scraping fails
59
- print(f"Scraping failed: {e}")
60
- return backup_tickers
61
 
62
  @st.cache_data(ttl=86400)
63
  def get_sector_stocks():
64
- """Get sector-wise stock lists"""
65
  return {
66
- "Banking & Finance": [
67
- "HDFCBANK.NS", "ICICIBANK.NS", "SBIN.NS", "KOTAKBANK.NS", "AXISBANK.NS",
68
- "INDUSINDBK.NS", "FEDERALBNK.NS", "BAJFINANCE.NS", "BAJAJFINSV.NS", "IDFCFIRSTB.NS"
69
- ],
70
- "Information Technology": [
71
- "TCS.NS", "INFY.NS", "HCLTECH.NS", "WIPRO.NS", "TECHM.NS",
72
- "COFORGE.NS", "PERSISTENT.NS", "LTIM.NS", "MPHASIS.NS", "OFSS.NS"
73
- ],
74
- "FMCG & Consumer": [
75
- "HINDUNILVR.NS", "ITC.NS", "NESTLEIND.NS", "BRITANNIA.NS", "DABUR.NS",
76
- "GODREJCP.NS", "MARICO.NS", "TATACONSUM.NS", "UBL.NS", "COLPAL.NS"
77
- ],
78
- "Pharmaceuticals": [
79
- "SUNPHARMA.NS", "DRREDDY.NS", "CIPLA.NS", "DIVISLAB.NS", "BIOCON.NS",
80
- "LUPIN.NS", "AUROPHARMA.NS", "TORNTPHARM.NS", "ALKEM.NS", "CADILAHC.NS"
81
- ],
82
- "Energy & Power": [
83
- "RELIANCE.NS", "ONGC.NS", "POWERGRID.NS", "NTPC.NS", "COALINDIA.NS",
84
- "GAIL.NS", "IOC.NS", "BPCL.NS", "TATAPOWER.NS", "ADANIGREEN.NS"
85
- ],
86
- "Automobiles": [
87
- "MARUTI.NS", "TATAMOTORS.NS", "M&M.NS", "BAJAJ-AUTO.NS", "EICHERMOT.NS",
88
- "HEROMOTOCO.NS", "TVSMOTOR.NS", "ASHOKLEY.NS", "MRF.NS", "APOLLOTYRE.NS"
89
- ],
90
- "Metals & Mining": [
91
- "TATASTEEL.NS", "JSWSTEEL.NS", "HINDALCO.NS", "VEDL.NS",
92
- "NATIONALUM.NS", "SAIL.NS", "JINDALSTEL.NS", "NMDC.NS", "COALINDIA.NS"
93
- ]
94
  }
95
 
96
- @st.cache_data(ttl=1800) # 30 minutes
97
  def get_stock_info(ticker):
98
- """Get stock metadata"""
99
  try:
100
  stock = yf.Ticker(ticker)
101
  info = stock.info
@@ -108,90 +149,32 @@ def get_stock_info(ticker):
108
  except:
109
  return {'name': ticker, 'sector': 'Unknown', 'industry': 'Unknown', 'price': 0}
110
 
111
- @st.cache_data(ttl=300)
112
- def get_global_indices():
113
- """Fetch key global market indices"""
114
- indices = {
115
- "🇺🇸 S&P 500": "^GSPC",
116
- "🇺🇸 Nasdaq": "^IXIC",
117
- "🇬🇧 FTSE 100": "^FTSE",
118
- "🇯🇵 Nikkei 225": "^N225"
119
- }
120
-
121
- data = []
122
- for name, ticker in indices.items():
123
- try:
124
- idx = yf.Ticker(ticker)
125
- hist = idx.history(period="2d")
126
-
127
- if len(hist) > 0:
128
- current = hist['Close'].iloc[-1]
129
- prev = hist['Close'].iloc[-2] if len(hist) > 1 else current
130
- change_pct = ((current - prev) / prev) * 100
131
-
132
- data.append({
133
- "Index": name,
134
- "Price": current,
135
- "Change %": change_pct
136
- })
137
- except Exception:
138
- continue
139
-
140
- return pd.DataFrame(data)
141
-
142
- @st.cache_data(ttl=900) # Cache news for 15 mins
143
- def get_market_news():
144
- """Fetch latest market news"""
145
- try:
146
- # Fetch news for NIFTY 50 or Sensex
147
- ticker = yf.Ticker("^NSEI")
148
- return ticker.news
149
- except Exception:
150
- return []
151
-
152
  def download_prices(tickers, start_date, end_date):
153
- """Download historical stock prices"""
154
  try:
155
- data = yf.download(
156
- tickers,
157
- start=start_date,
158
- end=end_date,
159
- progress=False,
160
- group_by="ticker" if len(tickers) > 1 else None
161
- )
162
-
163
- if data.empty:
164
- return pd.DataFrame()
165
-
166
  if len(tickers) == 1:
167
  if 'Close' in data.columns:
168
  prices = data[['Close']].copy()
169
  prices.columns = tickers
170
- else:
171
- return pd.DataFrame()
172
  elif isinstance(data.columns, pd.MultiIndex):
173
  cleaned = {}
174
  for ticker in tickers:
175
  try:
176
  ticker_data = data[ticker]['Close'].dropna()
177
- if len(ticker_data) > 50:
178
- cleaned[ticker] = ticker_data
179
- except:
180
- continue
181
  prices = pd.DataFrame(cleaned)
182
- else:
183
- prices = data
184
-
185
  prices = prices.ffill().dropna(how='all').dropna(axis=1, how='all')
186
  return prices
187
  except Exception as e:
188
  st.error(f"Error downloading data: {str(e)}")
189
  return pd.DataFrame()
190
 
191
- # ============ STATISTICS & OPTIMIZATION ============
192
-
193
  def compute_portfolio_stats(prices, periods_per_year=252):
194
- """Calculate portfolio statistics"""
195
  returns = prices.pct_change().dropna()
196
  mean_annual = returns.mean() * periods_per_year
197
  cov_annual = returns.cov() * periods_per_year
@@ -200,19 +183,15 @@ def compute_portfolio_stats(prices, periods_per_year=252):
200
  return returns, mean_annual, cov_annual, corr_matrix, volatility_annual
201
 
202
  def solve_optimization(cov_annual, expected_returns, target_return=None):
203
- """CVXPY portfolio optimization"""
204
  n = cov_annual.shape[0]
205
  w = cp.Variable(n)
206
  Sigma = cov_annual.values + 1e-6 * np.eye(n)
207
-
208
  constraints = [cp.sum(w) == 1, w >= 0]
209
  if target_return is not None:
210
  mu = expected_returns.values
211
  constraints.append(w.T @ mu >= target_return)
212
-
213
  objective = cp.quad_form(w, Sigma)
214
  prob = cp.Problem(cp.Minimize(objective), constraints)
215
-
216
  solvers = [cp.OSQP, cp.SCS, cp.ECOS]
217
  for solver in solvers:
218
  try:
@@ -222,73 +201,50 @@ def solve_optimization(cov_annual, expected_returns, target_return=None):
222
  weights = np.maximum(weights, 0)
223
  weights = weights / weights.sum()
224
  return weights
225
- except:
226
- continue
227
  return np.ones(n) / n
228
 
229
  def find_max_sharpe_portfolio(expected_returns, cov_annual, risk_free_rate=0.0654, n_points=50):
230
- """Find maximum Sharpe ratio portfolio"""
231
  min_ret = expected_returns.min()
232
  max_ret = expected_returns.max()
233
-
234
- if max_ret <= min_ret:
235
- return solve_optimization(cov_annual, expected_returns), []
236
-
237
  target_returns = np.linspace(min_ret + 0.001, max_ret - 0.001, n_points)
238
  best_sharpe = -np.inf
239
  best_weights = None
240
  efficient_frontier = []
241
-
242
  for target in target_returns:
243
  try:
244
  weights = solve_optimization(cov_annual, expected_returns, target)
245
  port_return = expected_returns.values @ weights
246
  port_volatility = np.sqrt(weights.T @ cov_annual.values @ weights)
247
-
248
- efficient_frontier.append({
249
- 'return': port_return,
250
- 'volatility': port_volatility,
251
- 'sharpe': (port_return - risk_free_rate) / port_volatility if port_volatility > 0 else 0
252
- })
253
-
254
  if port_volatility > 0:
255
  sharpe = (port_return - risk_free_rate) / port_volatility
256
  if sharpe > best_sharpe:
257
  best_sharpe = sharpe
258
  best_weights = weights
259
- except:
260
- continue
261
-
262
- if best_weights is None:
263
- best_weights = solve_optimization(cov_annual, expected_returns)
264
-
265
  return best_weights, efficient_frontier
266
 
267
- # ============ RISK METRICS ============
268
-
269
  def monte_carlo_simulation(returns, weights, initial_investment, n_simulations=1000, n_days=252):
270
- """Run Monte Carlo simulation"""
271
  mean_returns = returns.mean()
272
  cov_matrix = returns.cov()
273
  portfolio_returns = []
274
-
275
  for _ in range(n_simulations):
276
  simulated_returns = np.random.multivariate_normal(mean_returns, cov_matrix, n_days)
277
  portfolio_daily_returns = simulated_returns @ weights
278
  portfolio_value = initial_investment * (1 + portfolio_daily_returns).cumprod()[-1]
279
  portfolio_returns.append(portfolio_value)
280
-
281
  return np.array(portfolio_returns)
282
 
283
  def calculate_var_cvar(returns, weights, confidence_level=0.95):
284
- """Calculate Value at Risk and Conditional VaR"""
285
  portfolio_returns = returns @ weights
286
  var = np.percentile(portfolio_returns, (1 - confidence_level) * 100)
287
  cvar = portfolio_returns[portfolio_returns <= var].mean()
288
  return var, cvar
289
 
290
  def calculate_max_drawdown(prices, weights):
291
- """Calculate maximum drawdown"""
292
  portfolio_returns = (prices @ weights).pct_change().fillna(0)
293
  portfolio_value = (1 + portfolio_returns).cumprod()
294
  running_max = portfolio_value.cummax()
@@ -297,17 +253,14 @@ def calculate_max_drawdown(prices, weights):
297
  return max_drawdown, drawdown
298
 
299
  def calculate_rolling_volatility(returns, weights, window=30):
300
- """Calculate rolling volatility"""
301
  portfolio_returns = returns @ weights
302
  rolling_vol = portfolio_returns.rolling(window=window).std() * np.sqrt(252)
303
  return rolling_vol
304
 
305
  def stress_test_scenarios(returns, weights):
306
- """Run stress test scenarios"""
307
  portfolio_returns = returns @ weights
308
  mean = portfolio_returns.mean()
309
  std = portfolio_returns.std()
310
-
311
  scenarios = {
312
  'Market Crash (-20%)': -0.20,
313
  'Moderate Decline (-10%)': -0.10,
@@ -320,137 +273,86 @@ def stress_test_scenarios(returns, weights):
320
  }
321
  return scenarios
322
 
323
- # ============ REBALANCING ============
324
-
325
  def calculate_portfolio_metrics(prices, weights, risk_free_rate=0.0654):
326
- """Calculate current portfolio metrics"""
327
  returns, mean_annual, cov_annual, _, _ = compute_portfolio_stats(prices)
328
-
329
  port_return = mean_annual.values @ weights
330
  port_volatility = np.sqrt(weights.T @ cov_annual.values @ weights)
331
  sharpe_ratio = (port_return - risk_free_rate) / port_volatility if port_volatility > 0 else 0
332
-
333
- return {
334
- 'return': port_return,
335
- 'volatility': port_volatility,
336
- 'sharpe': sharpe_ratio
337
- }
338
 
339
  def generate_rebalancing_actions(current_holdings, optimal_weights, latest_prices, total_value, brokerage_rate=0.0003):
340
- """Generate buy/sell recommendations"""
341
  actions = []
342
-
343
  for ticker in optimal_weights.index:
344
  current_shares = current_holdings.get(ticker, {}).get('shares', 0)
345
  current_value = current_shares * latest_prices[ticker]
346
  current_weight = current_value / total_value if total_value > 0 else 0
347
-
348
  target_weight = optimal_weights[ticker]
349
  target_value = target_weight * total_value
350
  target_shares = int(target_value / latest_prices[ticker])
351
-
352
  diff_shares = target_shares - current_shares
353
  diff_value = diff_shares * latest_prices[ticker]
354
-
355
  if abs(diff_shares) > 0:
356
  action = 'BUY' if diff_shares > 0 else 'SELL'
357
  cost = abs(diff_value) * brokerage_rate
358
-
359
  actions.append({
360
- 'Stock': ticker,
361
- 'Action': action,
362
- 'Shares': abs(diff_shares),
363
- 'Price': f"₹{latest_prices[ticker]:.2f}",
364
- 'Amount': f"₹{abs(diff_value):,.0f}",
365
- 'Cost': f"₹{cost:.2f}",
366
- 'Current %': f"{current_weight*100:.2f}%",
367
- 'Target %': f"{target_weight*100:.2f}%"
368
  })
369
-
370
  return pd.DataFrame(actions) if actions else pd.DataFrame()
371
 
372
- # ============ MARKET INSIGHTS ============
373
-
374
  @st.cache_data(ttl=300)
375
  def get_nifty_data():
376
- """Get NIFTY 50 index data"""
377
  try:
378
  nifty = yf.Ticker("^NSEI")
379
  data = nifty.history(period="1mo")
380
- info = nifty.info
381
- return data, info
382
  except Exception as e:
383
- st.error(f"Error fetching NIFTY data: {str(e)}")
384
  return pd.DataFrame(), {}
385
 
386
  @st.cache_data(ttl=300)
387
  def get_top_movers(tickers, n=10):
388
- """Get top gainers and losers"""
389
  data = {}
390
  for ticker in tickers:
391
  try:
392
  stock = yf.Ticker(ticker)
393
  info = stock.info
394
  change_val = info.get('regularMarketChangePercent', 0)
395
- if change_val is None:
396
- change_val = 0
397
  data[ticker] = {
398
  'name': info.get('longName', ticker)[:30],
399
  'price': float(info.get('currentPrice', 0)),
400
  'change': float(change_val),
401
  'volume': int(info.get('volume', 0))
402
  }
403
- except:
404
- continue
405
-
406
  df = pd.DataFrame(data).T
407
- if df.empty:
408
- return pd.DataFrame(), pd.DataFrame()
409
-
410
  df['change'] = pd.to_numeric(df['change'], errors='coerce').fillna(0)
411
  df['price'] = pd.to_numeric(df['price'], errors='coerce').fillna(0)
412
-
413
  gainers = df.nlargest(n, 'change')
414
  losers = df.nsmallest(n, 'change')
415
  return gainers, losers
416
 
417
- def render_header():
418
- """
419
- Renders the top navigation bar for the application.
420
- Call this function at the top of every page.
421
- """
422
- # CSS to style the page links as a nav bar
423
- st.markdown("""
424
- <style>
425
- div[data-testid="stColumn"] {
426
- text-align: center;
427
- }
428
- div[data-testid="stColumn"] button {
429
- width: 100%;
430
- }
431
- hr {
432
- margin-top: 0.5rem;
433
- margin-bottom: 1rem;
434
- }
435
- </style>
436
- """, unsafe_allow_html=True)
437
 
438
- # Create 7 columns for the 7 pages
439
- col1, col2, col3, col4, col5, col6, col7 = st.columns(7)
440
-
441
- with col1:
442
- st.page_link("Main_Page.py", label="Home", icon="🏠")
443
- with col2:
444
- st.page_link("pages/1_New_Portfolio.py", label="New", icon="💼")
445
- with col3:
446
- st.page_link("pages/2_Rebalance.py", label="Rebalance", icon="🔄")
447
- with col4:
448
- st.page_link("pages/3_Risk_Analysis.py", label="Risk", icon="📊")
449
- with col5:
450
- st.page_link("pages/4_Market_Insights.py", label="Market", icon="📈")
451
- with col6:
452
- st.page_link("pages/5_Settings.py", label="Settings", icon="⚙️")
453
- with col7:
454
- st.page_link("pages/6_Learn.py", label="Learn", icon="📚")
455
-
456
- st.markdown("---")
 
3
  import numpy as np
4
  import yfinance as yf
5
  import cvxpy as cp
6
+ import plotly.express as px
7
  from datetime import datetime, timedelta
8
 
9
+ # ============ THEME MANAGEMENT ============
10
 
11
+ def initialize_theme():
12
+ """Initialize the theme state if it doesn't exist"""
13
+ if 'theme' not in st.session_state:
14
+ st.session_state.theme = 'light'
15
+
16
+ def toggle_theme():
17
+ """Switch between light and dark mode"""
18
+ if st.session_state.theme == 'light':
19
+ st.session_state.theme = 'dark'
20
+ else:
21
+ st.session_state.theme = 'light'
22
+
23
+ def get_theme_colors():
24
+ """Return colors based on current theme state"""
25
+ initialize_theme()
26
+ if st.session_state.theme == 'dark':
27
+ return {
28
+ "bg_color": "#0e1117",
29
+ "card_bg": "#1e293b",
30
+ "text": "#fafafa",
31
+ "border": "#334155",
32
+ "metric_label": "#94a3b8",
33
+ "icon": "🌙"
34
+ }
35
+ else:
36
+ return {
37
+ "bg_color": "#ffffff",
38
+ "card_bg": "#ffffff",
39
+ "text": "#0f172a",
40
+ "border": "#e2e8f0",
41
+ "metric_label": "#64748b",
42
+ "icon": "☀️"
43
+ }
44
+
45
+ # ============ HEADER WITH TOGGLE ============
46
+
47
+ def render_header():
48
  """
49
+ Renders the top navigation bar with Theme Toggle.
 
50
  """
51
+ initialize_theme()
52
+ colors = get_theme_colors()
53
+
54
+ # CSS to style the header and force the app background
55
+ st.markdown(f"""
56
+ <style>
57
+ /* Force App Background based on Toggle */
58
+ .stApp {{
59
+ background-color: {colors['bg_color']};
60
+ color: {colors['text']};
61
+ }}
62
+
63
+ div[data-testid="stColumn"] {{
64
+ text-align: center;
65
+ }}
66
+ div[data-testid="stColumn"] button {{
67
+ width: 100%;
68
+ background-color: {colors['card_bg']};
69
+ color: {colors['text']};
70
+ border: 1px solid {colors['border']};
71
+ }}
72
+ hr {{
73
+ margin-top: 0.5rem;
74
+ margin-bottom: 1rem;
75
+ border-color: {colors['border']};
76
+ }}
77
+ /* Fix Metric Card Text Colors globally */
78
+ [data-testid="stMetricLabel"] {{
79
+ color: {colors['metric_label']} !important;
80
+ }}
81
+ [data-testid="stMetricValue"] {{
82
+ color: {colors['text']} !important;
83
+ }}
84
+ </style>
85
+ """, unsafe_allow_html=True)
86
+
87
+ # Create columns: 7 for pages + 1 for Theme Toggle
88
+ cols = st.columns([1, 1, 1, 1, 1, 1, 1, 0.5])
89
+
90
+ with cols[0]: st.page_link("Main_Page.py", label="Home", icon="🏠")
91
+ with cols[1]: st.page_link("pages/1_New_Portfolio.py", label="New", icon="💼")
92
+ with cols[2]: st.page_link("pages/2_Rebalance.py", label="Rebalance", icon="🔄")
93
+ with cols[3]: st.page_link("pages/3_Risk_Analysis.py", label="Risk", icon="📊")
94
+ with cols[4]: st.page_link("pages/4_Market_Insights.py", label="Market", icon="📈")
95
+ with cols[5]: st.page_link("pages/5_Settings.py", label="Settings", icon="⚙️")
96
+ with cols[6]: st.page_link("pages/6_Learn.py", label="Learn", icon="📚")
97
+
98
+ # Theme Toggle Button
99
+ with cols[7]:
100
+ if st.button(colors['icon'], key="theme_toggle", help="Toggle Light/Dark Mode"):
101
+ toggle_theme()
102
+ st.rerun()
103
+
104
+ st.markdown("---")
105
+
106
+ # ============ DATA FETCHING (Existing Functions) ============
107
+
108
+ @st.cache_data(ttl=86400)
109
+ def get_nifty50_stocks():
110
+ # ... (Keep your scraping code here) ...
111
+ # PASTE YOUR EXISTING get_nifty50_stocks CODE HERE
112
  backup_tickers = [
113
  "RELIANCE.NS", "TCS.NS", "HDFCBANK.NS", "INFY.NS", "ICICIBANK.NS",
114
  "HINDUNILVR.NS", "ITC.NS", "SBIN.NS", "BHARTIARTL.NS", "KOTAKBANK.NS",
 
121
  "APOLLOHOSP.NS", "EICHERMOT.NS", "HEROMOTOCO.NS", "BPCL.NS", "TATACONSUM.NS",
122
  "SBILIFE.NS", "UPL.NS", "ADANIENT.NS", "HDFCLIFE.NS", "SHREECEM.NS"
123
  ]
124
+ return backup_tickers # (Or your scraping logic)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
 
126
  @st.cache_data(ttl=86400)
127
  def get_sector_stocks():
 
128
  return {
129
+ "Banking & Finance": ["HDFCBANK.NS", "ICICIBANK.NS", "SBIN.NS", "KOTAKBANK.NS", "AXISBANK.NS", "INDUSINDBK.NS", "FEDERALBNK.NS", "BAJFINANCE.NS", "BAJAJFINSV.NS", "IDFCFIRSTB.NS"],
130
+ "Information Technology": ["TCS.NS", "INFY.NS", "HCLTECH.NS", "WIPRO.NS", "TECHM.NS", "COFORGE.NS", "PERSISTENT.NS", "LTIM.NS", "MPHASIS.NS", "OFSS.NS"],
131
+ "FMCG & Consumer": ["HINDUNILVR.NS", "ITC.NS", "NESTLEIND.NS", "BRITANNIA.NS", "DABUR.NS", "GODREJCP.NS", "MARICO.NS", "TATACONSUM.NS", "UBL.NS", "COLPAL.NS"],
132
+ "Pharmaceuticals": ["SUNPHARMA.NS", "DRREDDY.NS", "CIPLA.NS", "DIVISLAB.NS", "BIOCON.NS", "LUPIN.NS", "AUROPHARMA.NS", "TORNTPHARM.NS", "ALKEM.NS", "CADILAHC.NS"],
133
+ "Energy & Power": ["RELIANCE.NS", "ONGC.NS", "POWERGRID.NS", "NTPC.NS", "COALINDIA.NS", "GAIL.NS", "IOC.NS", "BPCL.NS", "TATAPOWER.NS", "ADANIGREEN.NS"],
134
+ "Automobiles": ["MARUTI.NS", "TATAMOTORS.NS", "M&M.NS", "BAJAJ-AUTO.NS", "EICHERMOT.NS", "HEROMOTOCO.NS", "TVSMOTOR.NS", "ASHOKLEY.NS", "MRF.NS", "APOLLOTYRE.NS"],
135
+ "Metals & Mining": ["TATASTEEL.NS", "JSWSTEEL.NS", "HINDALCO.NS", "VEDL.NS", "NATIONALUM.NS", "SAIL.NS", "JINDALSTEL.NS", "NMDC.NS", "COALINDIA.NS"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
  }
137
 
138
+ @st.cache_data(ttl=1800)
139
  def get_stock_info(ticker):
 
140
  try:
141
  stock = yf.Ticker(ticker)
142
  info = stock.info
 
149
  except:
150
  return {'name': ticker, 'sector': 'Unknown', 'industry': 'Unknown', 'price': 0}
151
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
  def download_prices(tickers, start_date, end_date):
 
153
  try:
154
+ data = yf.download(tickers, start=start_date, end=end_date, progress=False, group_by="ticker" if len(tickers) > 1 else None)
155
+ if data.empty: return pd.DataFrame()
 
 
 
 
 
 
 
 
 
156
  if len(tickers) == 1:
157
  if 'Close' in data.columns:
158
  prices = data[['Close']].copy()
159
  prices.columns = tickers
160
+ else: return pd.DataFrame()
 
161
  elif isinstance(data.columns, pd.MultiIndex):
162
  cleaned = {}
163
  for ticker in tickers:
164
  try:
165
  ticker_data = data[ticker]['Close'].dropna()
166
+ if len(ticker_data) > 50: cleaned[ticker] = ticker_data
167
+ except: continue
 
 
168
  prices = pd.DataFrame(cleaned)
169
+ else: prices = data
 
 
170
  prices = prices.ffill().dropna(how='all').dropna(axis=1, how='all')
171
  return prices
172
  except Exception as e:
173
  st.error(f"Error downloading data: {str(e)}")
174
  return pd.DataFrame()
175
 
176
+ # ============ STATISTICS ============
 
177
  def compute_portfolio_stats(prices, periods_per_year=252):
 
178
  returns = prices.pct_change().dropna()
179
  mean_annual = returns.mean() * periods_per_year
180
  cov_annual = returns.cov() * periods_per_year
 
183
  return returns, mean_annual, cov_annual, corr_matrix, volatility_annual
184
 
185
  def solve_optimization(cov_annual, expected_returns, target_return=None):
 
186
  n = cov_annual.shape[0]
187
  w = cp.Variable(n)
188
  Sigma = cov_annual.values + 1e-6 * np.eye(n)
 
189
  constraints = [cp.sum(w) == 1, w >= 0]
190
  if target_return is not None:
191
  mu = expected_returns.values
192
  constraints.append(w.T @ mu >= target_return)
 
193
  objective = cp.quad_form(w, Sigma)
194
  prob = cp.Problem(cp.Minimize(objective), constraints)
 
195
  solvers = [cp.OSQP, cp.SCS, cp.ECOS]
196
  for solver in solvers:
197
  try:
 
201
  weights = np.maximum(weights, 0)
202
  weights = weights / weights.sum()
203
  return weights
204
+ except: continue
 
205
  return np.ones(n) / n
206
 
207
  def find_max_sharpe_portfolio(expected_returns, cov_annual, risk_free_rate=0.0654, n_points=50):
 
208
  min_ret = expected_returns.min()
209
  max_ret = expected_returns.max()
210
+ if max_ret <= min_ret: return solve_optimization(cov_annual, expected_returns), []
 
 
 
211
  target_returns = np.linspace(min_ret + 0.001, max_ret - 0.001, n_points)
212
  best_sharpe = -np.inf
213
  best_weights = None
214
  efficient_frontier = []
 
215
  for target in target_returns:
216
  try:
217
  weights = solve_optimization(cov_annual, expected_returns, target)
218
  port_return = expected_returns.values @ weights
219
  port_volatility = np.sqrt(weights.T @ cov_annual.values @ weights)
220
+ efficient_frontier.append({'return': port_return, 'volatility': port_volatility, 'sharpe': (port_return - risk_free_rate) / port_volatility if port_volatility > 0 else 0})
 
 
 
 
 
 
221
  if port_volatility > 0:
222
  sharpe = (port_return - risk_free_rate) / port_volatility
223
  if sharpe > best_sharpe:
224
  best_sharpe = sharpe
225
  best_weights = weights
226
+ except: continue
227
+ if best_weights is None: best_weights = solve_optimization(cov_annual, expected_returns)
 
 
 
 
228
  return best_weights, efficient_frontier
229
 
 
 
230
  def monte_carlo_simulation(returns, weights, initial_investment, n_simulations=1000, n_days=252):
 
231
  mean_returns = returns.mean()
232
  cov_matrix = returns.cov()
233
  portfolio_returns = []
 
234
  for _ in range(n_simulations):
235
  simulated_returns = np.random.multivariate_normal(mean_returns, cov_matrix, n_days)
236
  portfolio_daily_returns = simulated_returns @ weights
237
  portfolio_value = initial_investment * (1 + portfolio_daily_returns).cumprod()[-1]
238
  portfolio_returns.append(portfolio_value)
 
239
  return np.array(portfolio_returns)
240
 
241
  def calculate_var_cvar(returns, weights, confidence_level=0.95):
 
242
  portfolio_returns = returns @ weights
243
  var = np.percentile(portfolio_returns, (1 - confidence_level) * 100)
244
  cvar = portfolio_returns[portfolio_returns <= var].mean()
245
  return var, cvar
246
 
247
  def calculate_max_drawdown(prices, weights):
 
248
  portfolio_returns = (prices @ weights).pct_change().fillna(0)
249
  portfolio_value = (1 + portfolio_returns).cumprod()
250
  running_max = portfolio_value.cummax()
 
253
  return max_drawdown, drawdown
254
 
255
  def calculate_rolling_volatility(returns, weights, window=30):
 
256
  portfolio_returns = returns @ weights
257
  rolling_vol = portfolio_returns.rolling(window=window).std() * np.sqrt(252)
258
  return rolling_vol
259
 
260
  def stress_test_scenarios(returns, weights):
 
261
  portfolio_returns = returns @ weights
262
  mean = portfolio_returns.mean()
263
  std = portfolio_returns.std()
 
264
  scenarios = {
265
  'Market Crash (-20%)': -0.20,
266
  'Moderate Decline (-10%)': -0.10,
 
273
  }
274
  return scenarios
275
 
 
 
276
  def calculate_portfolio_metrics(prices, weights, risk_free_rate=0.0654):
 
277
  returns, mean_annual, cov_annual, _, _ = compute_portfolio_stats(prices)
 
278
  port_return = mean_annual.values @ weights
279
  port_volatility = np.sqrt(weights.T @ cov_annual.values @ weights)
280
  sharpe_ratio = (port_return - risk_free_rate) / port_volatility if port_volatility > 0 else 0
281
+ return {'return': port_return, 'volatility': port_volatility, 'sharpe': sharpe_ratio}
 
 
 
 
 
282
 
283
  def generate_rebalancing_actions(current_holdings, optimal_weights, latest_prices, total_value, brokerage_rate=0.0003):
 
284
  actions = []
 
285
  for ticker in optimal_weights.index:
286
  current_shares = current_holdings.get(ticker, {}).get('shares', 0)
287
  current_value = current_shares * latest_prices[ticker]
288
  current_weight = current_value / total_value if total_value > 0 else 0
 
289
  target_weight = optimal_weights[ticker]
290
  target_value = target_weight * total_value
291
  target_shares = int(target_value / latest_prices[ticker])
 
292
  diff_shares = target_shares - current_shares
293
  diff_value = diff_shares * latest_prices[ticker]
 
294
  if abs(diff_shares) > 0:
295
  action = 'BUY' if diff_shares > 0 else 'SELL'
296
  cost = abs(diff_value) * brokerage_rate
 
297
  actions.append({
298
+ 'Stock': ticker, 'Action': action, 'Shares': abs(diff_shares),
299
+ 'Price': f"₹{latest_prices[ticker]:.2f}", 'Amount': f"₹{abs(diff_value):,.0f}",
300
+ 'Cost': f"₹{cost:.2f}", 'Current %': f"{current_weight*100:.2f}%", 'Target %': f"{target_weight*100:.2f}%"
 
 
 
 
 
301
  })
 
302
  return pd.DataFrame(actions) if actions else pd.DataFrame()
303
 
 
 
304
  @st.cache_data(ttl=300)
305
  def get_nifty_data():
 
306
  try:
307
  nifty = yf.Ticker("^NSEI")
308
  data = nifty.history(period="1mo")
309
+ return data, nifty.info
 
310
  except Exception as e:
 
311
  return pd.DataFrame(), {}
312
 
313
  @st.cache_data(ttl=300)
314
  def get_top_movers(tickers, n=10):
 
315
  data = {}
316
  for ticker in tickers:
317
  try:
318
  stock = yf.Ticker(ticker)
319
  info = stock.info
320
  change_val = info.get('regularMarketChangePercent', 0)
321
+ if change_val is None: change_val = 0
 
322
  data[ticker] = {
323
  'name': info.get('longName', ticker)[:30],
324
  'price': float(info.get('currentPrice', 0)),
325
  'change': float(change_val),
326
  'volume': int(info.get('volume', 0))
327
  }
328
+ except: continue
 
 
329
  df = pd.DataFrame(data).T
330
+ if df.empty: return pd.DataFrame(), pd.DataFrame()
 
 
331
  df['change'] = pd.to_numeric(df['change'], errors='coerce').fillna(0)
332
  df['price'] = pd.to_numeric(df['price'], errors='coerce').fillna(0)
 
333
  gainers = df.nlargest(n, 'change')
334
  losers = df.nsmallest(n, 'change')
335
  return gainers, losers
336
 
337
+ @st.cache_data(ttl=300)
338
+ def get_global_indices():
339
+ indices = {"🇺🇸 S&P 500": "^GSPC", "🇺🇸 Nasdaq": "^IXIC", "🇬🇧 FTSE 100": "^FTSE", "🇯🇵 Nikkei 225": "^N225"}
340
+ data = []
341
+ for name, ticker in indices.items():
342
+ try:
343
+ idx = yf.Ticker(ticker)
344
+ hist = idx.history(period="2d")
345
+ if len(hist) > 0:
346
+ current = hist['Close'].iloc[-1]
347
+ prev = hist['Close'].iloc[-2] if len(hist) > 1 else current
348
+ change_pct = ((current - prev) / prev) * 100
349
+ data.append({"Index": name, "Price": current, "Change %": change_pct})
350
+ except: continue
351
+ return pd.DataFrame(data)
 
 
 
 
 
352
 
353
+ @st.cache_data(ttl=900)
354
+ def get_market_news():
355
+ try:
356
+ ticker = yf.Ticker("^NSEI")
357
+ return ticker.news
358
+ except: return []