pvyas96 commited on
Commit
1544014
·
verified ·
1 Parent(s): 86a0207

Create utils.py

Browse files
Files changed (1) hide show
  1. src/utils.py +338 -0
src/utils.py ADDED
@@ -0,0 +1,338 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ 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) # 24 hours
11
+ def get_nifty50_stocks():
12
+ """Fetch NIFTY 50 constituent stocks"""
13
+ return [
14
+ "RELIANCE.NS", "TCS.NS", "HDFCBANK.NS", "INFY.NS", "ICICIBANK.NS",
15
+ "HINDUNILVR.NS", "ITC.NS", "SBIN.NS", "BHARTIARTL.NS", "KOTAKBANK.NS",
16
+ "LT.NS", "AXISBANK.NS", "ASIANPAINT.NS", "MARUTI.NS", "SUNPHARMA.NS",
17
+ "TITAN.NS", "BAJFINANCE.NS", "WIPRO.NS", "ULTRACEMCO.NS", "NESTLEIND.NS",
18
+ "HCLTECH.NS", "POWERGRID.NS", "NTPC.NS", "TECHM.NS", "ONGC.NS",
19
+ "M&M.NS", "TATAMOTORS.NS", "BAJAJFINSV.NS", "TATASTEEL.NS", "ADANIPORTS.NS",
20
+ "COALINDIA.NS", "INDUSINDBK.NS", "DRREDDY.NS", "JSWSTEEL.NS", "CIPLA.NS",
21
+ "BRITANNIA.NS", "BAJAJ-AUTO.NS", "DIVISLAB.NS", "GRASIM.NS", "HINDALCO.NS",
22
+ "APOLLOHOSP.NS", "EICHERMOT.NS", "HEROMOTOCO.NS", "BPCL.NS", "TATACONSUM.NS",
23
+ "SBILIFE.NS", "UPL.NS", "ADANIENT.NS", "HDFCLIFE.NS", "SHREECEM.NS"
24
+ ]
25
+
26
+ @st.cache_data(ttl=86400)
27
+ def get_sector_stocks():
28
+ """Get sector-wise stock lists"""
29
+ return {
30
+ "Banking & Finance": [
31
+ "HDFCBANK.NS", "ICICIBANK.NS", "SBIN.NS", "KOTAKBANK.NS", "AXISBANK.NS",
32
+ "INDUSINDBK.NS", "FEDERALBNK.NS", "BAJFINANCE.NS", "BAJAJFINSV.NS", "IDFCFIRSTB.NS"
33
+ ],
34
+ "Information Technology": [
35
+ "TCS.NS", "INFY.NS", "HCLTECH.NS", "WIPRO.NS", "TECHM.NS",
36
+ "COFORGE.NS", "PERSISTENT.NS", "LTIM.NS", "MPHASIS.NS", "OFSS.NS"
37
+ ],
38
+ "FMCG & Consumer": [
39
+ "HINDUNILVR.NS", "ITC.NS", "NESTLEIND.NS", "BRITANNIA.NS", "DABUR.NS",
40
+ "GODREJCP.NS", "MARICO.NS", "TATACONSUM.NS", "UBL.NS", "COLPAL.NS"
41
+ ],
42
+ "Pharmaceuticals": [
43
+ "SUNPHARMA.NS", "DRREDDY.NS", "CIPLA.NS", "DIVISLAB.NS", "BIOCON.NS",
44
+ "LUPIN.NS", "AUROPHARMA.NS", "TORNTPHARM.NS", "ALKEM.NS", "CADILAHC.NS"
45
+ ],
46
+ "Energy & Power": [
47
+ "RELIANCE.NS", "ONGC.NS", "POWERGRID.NS", "NTPC.NS", "COALINDIA.NS",
48
+ "GAIL.NS", "IOC.NS", "BPCL.NS", "TATAPOWER.NS", "ADANIGREEN.NS"
49
+ ],
50
+ "Automobiles": [
51
+ "MARUTI.NS", "TATAMOTORS.NS", "M&M.NS", "BAJAJ-AUTO.NS", "EICHERMOT.NS",
52
+ "HEROMOTOCO.NS", "TVSMOTOR.NS", "ASHOKLEY.NS", "MRF.NS", "APOLLOTYRE.NS"
53
+ ],
54
+ "Metals & Mining": [
55
+ "TATASTEEL.NS", "JSWSTEEL.NS", "HINDALCO.NS", "VEDL.NS",
56
+ "NATIONALUM.NS", "SAIL.NS", "JINDALSTEL.NS", "NMDC.NS", "COALINDIA.NS"
57
+ ]
58
+ }
59
+
60
+ @st.cache_data(ttl=1800) # 30 minutes
61
+ def get_stock_info(ticker):
62
+ """Get stock metadata"""
63
+ try:
64
+ stock = yf.Ticker(ticker)
65
+ info = stock.info
66
+ return {
67
+ 'name': info.get('longName', ticker),
68
+ 'sector': info.get('sector', 'Unknown'),
69
+ 'industry': info.get('industry', 'Unknown'),
70
+ 'price': info.get('currentPrice', 0),
71
+ }
72
+ except:
73
+ return {'name': ticker, 'sector': 'Unknown', 'industry': 'Unknown', 'price': 0}
74
+
75
+ def download_prices(tickers, start_date, end_date):
76
+ """Download historical stock prices"""
77
+ try:
78
+ data = yf.download(
79
+ tickers,
80
+ start=start_date,
81
+ end=end_date,
82
+ progress=False,
83
+ group_by="ticker" if len(tickers) > 1 else None
84
+ )
85
+
86
+ if data.empty:
87
+ return pd.DataFrame()
88
+
89
+ if len(tickers) == 1:
90
+ if 'Close' in data.columns:
91
+ prices = data[['Close']].copy()
92
+ prices.columns = tickers
93
+ else:
94
+ return pd.DataFrame()
95
+ elif isinstance(data.columns, pd.MultiIndex):
96
+ cleaned = {}
97
+ for ticker in tickers:
98
+ try:
99
+ ticker_data = data[ticker]['Close'].dropna()
100
+ if len(ticker_data) > 50:
101
+ cleaned[ticker] = ticker_data
102
+ except:
103
+ continue
104
+ prices = pd.DataFrame(cleaned)
105
+ else:
106
+ prices = data
107
+
108
+ prices = prices.ffill().dropna(how='all').dropna(axis=1, how='all')
109
+ return prices
110
+ except Exception as e:
111
+ st.error(f"Error downloading data: {str(e)}")
112
+ return pd.DataFrame()
113
+
114
+ # ============ STATISTICS & OPTIMIZATION ============
115
+
116
+ def compute_portfolio_stats(prices, periods_per_year=252):
117
+ """Calculate portfolio statistics"""
118
+ returns = prices.pct_change().dropna()
119
+ mean_annual = returns.mean() * periods_per_year
120
+ cov_annual = returns.cov() * periods_per_year
121
+ corr_matrix = returns.corr()
122
+ volatility_annual = returns.std() * np.sqrt(periods_per_year)
123
+ return returns, mean_annual, cov_annual, corr_matrix, volatility_annual
124
+
125
+ def solve_optimization(cov_annual, expected_returns, target_return=None):
126
+ """CVXPY portfolio optimization"""
127
+ n = cov_annual.shape[0]
128
+ w = cp.Variable(n)
129
+ Sigma = cov_annual.values + 1e-6 * np.eye(n)
130
+
131
+ constraints = [cp.sum(w) == 1, w >= 0]
132
+ if target_return is not None:
133
+ mu = expected_returns.values
134
+ constraints.append(w.T @ mu >= target_return)
135
+
136
+ objective = cp.quad_form(w, Sigma)
137
+ prob = cp.Problem(cp.Minimize(objective), constraints)
138
+
139
+ solvers = [cp.OSQP, cp.SCS, cp.ECOS]
140
+ for solver in solvers:
141
+ try:
142
+ prob.solve(solver=solver, verbose=False)
143
+ if w.value is not None and prob.status == cp.OPTIMAL:
144
+ weights = np.array(w.value).flatten()
145
+ weights = np.maximum(weights, 0)
146
+ weights = weights / weights.sum()
147
+ return weights
148
+ except:
149
+ continue
150
+ return np.ones(n) / n
151
+
152
+ def find_max_sharpe_portfolio(expected_returns, cov_annual, risk_free_rate=0.0654, n_points=50):
153
+ """Find maximum Sharpe ratio portfolio"""
154
+ min_ret = expected_returns.min()
155
+ max_ret = expected_returns.max()
156
+
157
+ if max_ret <= min_ret:
158
+ return solve_optimization(cov_annual, expected_returns), []
159
+
160
+ target_returns = np.linspace(min_ret + 0.001, max_ret - 0.001, n_points)
161
+ best_sharpe = -np.inf
162
+ best_weights = None
163
+ efficient_frontier = []
164
+
165
+ for target in target_returns:
166
+ try:
167
+ weights = solve_optimization(cov_annual, expected_returns, target)
168
+ port_return = expected_returns.values @ weights
169
+ port_volatility = np.sqrt(weights.T @ cov_annual.values @ weights)
170
+
171
+ efficient_frontier.append({
172
+ 'return': port_return,
173
+ 'volatility': port_volatility,
174
+ 'sharpe': (port_return - risk_free_rate) / port_volatility if port_volatility > 0 else 0
175
+ })
176
+
177
+ if port_volatility > 0:
178
+ sharpe = (port_return - risk_free_rate) / port_volatility
179
+ if sharpe > best_sharpe:
180
+ best_sharpe = sharpe
181
+ best_weights = weights
182
+ except:
183
+ continue
184
+
185
+ if best_weights is None:
186
+ best_weights = solve_optimization(cov_annual, expected_returns)
187
+
188
+ return best_weights, efficient_frontier
189
+
190
+ # ============ RISK METRICS ============
191
+
192
+ def monte_carlo_simulation(returns, weights, initial_investment, n_simulations=1000, n_days=252):
193
+ """Run Monte Carlo simulation"""
194
+ mean_returns = returns.mean()
195
+ cov_matrix = returns.cov()
196
+ portfolio_returns = []
197
+
198
+ for _ in range(n_simulations):
199
+ simulated_returns = np.random.multivariate_normal(mean_returns, cov_matrix, n_days)
200
+ portfolio_daily_returns = simulated_returns @ weights
201
+ portfolio_value = initial_investment * (1 + portfolio_daily_returns).cumprod()[-1]
202
+ portfolio_returns.append(portfolio_value)
203
+
204
+ return np.array(portfolio_returns)
205
+
206
+ def calculate_var_cvar(returns, weights, confidence_level=0.95):
207
+ """Calculate Value at Risk and Conditional VaR"""
208
+ portfolio_returns = returns @ weights
209
+ var = np.percentile(portfolio_returns, (1 - confidence_level) * 100)
210
+ cvar = portfolio_returns[portfolio_returns <= var].mean()
211
+ return var, cvar
212
+
213
+ def calculate_max_drawdown(prices, weights):
214
+ """Calculate maximum drawdown"""
215
+ portfolio_returns = (prices @ weights).pct_change().fillna(0)
216
+ portfolio_value = (1 + portfolio_returns).cumprod()
217
+ running_max = portfolio_value.cummax()
218
+ drawdown = (portfolio_value - running_max) / running_max
219
+ max_drawdown = drawdown.min()
220
+ return max_drawdown, drawdown
221
+
222
+ def calculate_rolling_volatility(returns, weights, window=30):
223
+ """Calculate rolling volatility"""
224
+ portfolio_returns = returns @ weights
225
+ rolling_vol = portfolio_returns.rolling(window=window).std() * np.sqrt(252)
226
+ return rolling_vol
227
+
228
+ def stress_test_scenarios(returns, weights):
229
+ """Run stress test scenarios"""
230
+ portfolio_returns = returns @ weights
231
+ mean = portfolio_returns.mean()
232
+ std = portfolio_returns.std()
233
+
234
+ scenarios = {
235
+ 'Market Crash (-20%)': -0.20,
236
+ 'Moderate Decline (-10%)': -0.10,
237
+ 'Minor Correction (-5%)': -0.05,
238
+ 'Current Volatility': std,
239
+ 'Volatility Spike (2x)': std * 2,
240
+ 'Best Historical Day': portfolio_returns.max(),
241
+ 'Worst Historical Day': portfolio_returns.min(),
242
+ 'Mean Daily Return': mean
243
+ }
244
+ return scenarios
245
+
246
+ # ============ REBALANCING ============
247
+
248
+ def calculate_portfolio_metrics(prices, weights, risk_free_rate=0.0654):
249
+ """Calculate current portfolio metrics"""
250
+ returns, mean_annual, cov_annual, _, _ = compute_portfolio_stats(prices)
251
+
252
+ port_return = mean_annual.values @ weights
253
+ port_volatility = np.sqrt(weights.T @ cov_annual.values @ weights)
254
+ sharpe_ratio = (port_return - risk_free_rate) / port_volatility if port_volatility > 0 else 0
255
+
256
+ return {
257
+ 'return': port_return,
258
+ 'volatility': port_volatility,
259
+ 'sharpe': sharpe_ratio
260
+ }
261
+
262
+ def generate_rebalancing_actions(current_holdings, optimal_weights, latest_prices, total_value, brokerage_rate=0.0003):
263
+ """Generate buy/sell recommendations"""
264
+ actions = []
265
+
266
+ for ticker in optimal_weights.index:
267
+ current_shares = current_holdings.get(ticker, {}).get('shares', 0)
268
+ current_value = current_shares * latest_prices[ticker]
269
+ current_weight = current_value / total_value if total_value > 0 else 0
270
+
271
+ target_weight = optimal_weights[ticker]
272
+ target_value = target_weight * total_value
273
+ target_shares = int(target_value / latest_prices[ticker])
274
+
275
+ diff_shares = target_shares - current_shares
276
+ diff_value = diff_shares * latest_prices[ticker]
277
+
278
+ if abs(diff_shares) > 0:
279
+ action = 'BUY' if diff_shares > 0 else 'SELL'
280
+ cost = abs(diff_value) * brokerage_rate
281
+
282
+ actions.append({
283
+ 'Stock': ticker,
284
+ 'Action': action,
285
+ 'Shares': abs(diff_shares),
286
+ 'Price': f"₹{latest_prices[ticker]:.2f}",
287
+ 'Amount': f"₹{abs(diff_value):,.0f}",
288
+ 'Cost': f"₹{cost:.2f}",
289
+ 'Current %': f"{current_weight*100:.2f}%",
290
+ 'Target %': f"{target_weight*100:.2f}%"
291
+ })
292
+
293
+ return pd.DataFrame(actions) if actions else pd.DataFrame()
294
+
295
+ # ============ MARKET INSIGHTS ============
296
+
297
+ @st.cache_data(ttl=300)
298
+ def get_nifty_data():
299
+ """Get NIFTY 50 index data"""
300
+ try:
301
+ nifty = yf.Ticker("^NSEI")
302
+ data = nifty.history(period="1mo")
303
+ info = nifty.info
304
+ return data, info
305
+ except Exception as e:
306
+ st.error(f"Error fetching NIFTY data: {str(e)}")
307
+ return pd.DataFrame(), {}
308
+
309
+ @st.cache_data(ttl=300)
310
+ def get_top_movers(tickers, n=10):
311
+ """Get top gainers and losers"""
312
+ data = {}
313
+ for ticker in tickers:
314
+ try:
315
+ stock = yf.Ticker(ticker)
316
+ info = stock.info
317
+ change_val = info.get('regularMarketChangePercent', 0)
318
+ if change_val is None:
319
+ change_val = 0
320
+ data[ticker] = {
321
+ 'name': info.get('longName', ticker)[:30],
322
+ 'price': float(info.get('currentPrice', 0)),
323
+ 'change': float(change_val),
324
+ 'volume': int(info.get('volume', 0))
325
+ }
326
+ except:
327
+ continue
328
+
329
+ df = pd.DataFrame(data).T
330
+ if df.empty:
331
+ return pd.DataFrame(), pd.DataFrame()
332
+
333
+ df['change'] = pd.to_numeric(df['change'], errors='coerce').fillna(0)
334
+ df['price'] = pd.to_numeric(df['price'], errors='coerce').fillna(0)
335
+
336
+ gainers = df.nlargest(n, 'change')
337
+ losers = df.nsmallest(n, 'change')
338
+ return gainers, losers