eshan6704 commited on
Commit
f376ad9
·
verified ·
1 Parent(s): 24cd9e1

Update app/yahooinfo.py

Browse files
Files changed (1) hide show
  1. app/yahooinfo.py +108 -219
app/yahooinfo.py CHANGED
@@ -4,8 +4,6 @@
4
  import yfinance as yf
5
  import pandas as pd
6
  import traceback
7
- import time
8
-
9
  from datetime import datetime, timezone
10
 
11
  # persist helpers
@@ -16,16 +14,10 @@ from .persist import exists, load, save
16
  # Yahoo Finance info fetch (RAW)
17
  # ==============================
18
  def yfinfo(symbol):
19
- """
20
- Low-level Yahoo Finance info fetch.
21
- Returns raw dict or {"__error__": "..."}
22
- """
23
  try:
24
  t = yf.Ticker(symbol + ".NS")
25
  info = t.info
26
- if not info or not isinstance(info, dict):
27
- return {}
28
- return info
29
  except Exception as e:
30
  return {"__error__": str(e)}
31
 
@@ -45,29 +37,24 @@ SUBGROUP_ICONS = {
45
  MAIN_ICONS = {
46
  "Price / Volume": "📈",
47
  "Fundamentals": "📊",
48
- "Company Profile": "🏢"
 
49
  }
50
 
51
 
52
  # ==============================
53
- # Responsive column layout
54
  # ==============================
55
  def column_layout(html, min_width=320):
56
  return f"""
57
- <div style="
58
- display:grid;
59
- grid-template-columns:repeat(auto-fit,minmax({min_width}px,1fr));
60
- gap:10px;
61
- align-items:start;
62
- ">
63
  {html}
64
  </div>
65
  """
66
 
67
 
68
- # ==============================
69
- # Card renderer
70
- # ==============================
71
  def html_card(title, body, mini=False, shade=0):
72
  font = "12px" if mini else "14px"
73
  pad = "6px" if mini else "10px"
@@ -80,22 +67,17 @@ def html_card(title, body, mini=False, shade=0):
80
  ]
81
 
82
  return f"""
83
- <div style="
84
- background:{shades[shade%3]};
85
- border:1px solid #a3c0e0;
86
- border-radius:8px;
87
- padding:{pad};
88
- font-size:{font};
89
- box-shadow:0 2px 6px rgba(0,0,0,.08);
90
- ">
91
- <div style="
92
- background:{grads[shade%3]};
93
- color:white;
94
- padding:4px 8px;
95
- border-radius:6px;
96
- font-weight:600;
97
- margin-bottom:6px;
98
- ">
99
  {title}
100
  </div>
101
  {body}
@@ -106,58 +88,66 @@ def html_card(title, body, mini=False, shade=0):
106
  # ==============================
107
  # Formatting helpers
108
  # ==============================
109
- def format_number(x):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
  try:
111
- x = float(x)
112
- if abs(x) >= 100:
113
- return f"{x:,.0f}"
114
- if abs(x) >= 1:
115
- return f"{x:,.2f}"
116
- return f"{x:.4f}"
117
  except:
118
- return str(x)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
 
120
 
121
  # ==============================
122
- # Compact inline key:value view
123
  # ==============================
124
  def make_table(df):
125
- rows = ""
126
- for _, r in df.iterrows():
127
- color = "#0d1f3c"
128
- if any(x in r[0].lower() for x in ["chg", "%"]):
129
- try:
130
- color = "#0a7d32" if float(r[1]) >= 0 else "#b00020"
131
- except:
132
- pass
133
-
134
- rows += f"""
135
- <div style="
136
- display:flex;
137
- justify-content:space-between;
138
- gap:6px;
139
- padding:2px 0;
140
- border-bottom:1px dashed #bcd0ea;
141
- ">
142
- <span style="color:#1a4f8a;font-weight:500;">
143
- {r[0]}
144
- </span>
145
- <span style="
146
- color:{color};
147
- font-weight:600;
148
- background:#f1f6ff;
149
- padding:1px 6px;
150
- border-radius:4px;
151
- ">
152
- {r[1]}
153
- </span>
154
  </div>
155
  """
156
- return f"<div>{rows}</div>"
 
157
 
158
 
159
  # ==============================
160
- # Noise filtering
161
  # ==============================
162
  NOISE_KEYS = {
163
  "maxAge","priceHint","triggerable",
@@ -166,38 +156,11 @@ NOISE_KEYS = {
166
  "esgPopulated"
167
  }
168
 
169
- def is_noise(k):
170
- return k in NOISE_KEYS
171
 
172
 
173
  # ==============================
174
- # Duplicate resolver
175
- # ==============================
176
- DUPLICATE_PRIORITY = {
177
- "price": ["regularMarketPrice","currentPrice"],
178
- "prev": ["regularMarketPreviousClose","previousClose"],
179
- "open": ["regularMarketOpen","open"],
180
- "high": ["regularMarketDayHigh","dayHigh"],
181
- "low": ["regularMarketDayLow","dayLow"],
182
- "volume": ["regularMarketVolume","volume"]
183
- }
184
-
185
- def resolve_duplicates(data):
186
- resolved, used = {}, set()
187
- for keys in DUPLICATE_PRIORITY.values():
188
- for k in keys:
189
- if k in data:
190
- resolved[k] = data[k]
191
- used.update(keys)
192
- break
193
- for k,v in data.items():
194
- if k not in used:
195
- resolved[k] = v
196
- return resolved
197
-
198
-
199
- # ==============================
200
- # Short key names (DISPLAY)
201
  # ==============================
202
  SHORT_NAMES = {
203
  "regularMarketPrice":"Price",
@@ -208,43 +171,24 @@ SHORT_NAMES = {
208
  "regularMarketDayHigh":"High",
209
  "regularMarketDayLow":"Low",
210
  "regularMarketVolume":"Vol",
211
- "averageDailyVolume10Day":"AvgV10",
212
- "averageDailyVolume3Month":"AvgV3M",
213
- "fiftyDayAverage":"50DMA",
214
- "twoHundredDayAverage":"200DMA",
215
- "fiftyTwoWeekLow":"52WL",
216
- "fiftyTwoWeekHigh":"52WH",
217
  "beta":"Beta",
218
  "targetMeanPrice":"Target"
219
  }
220
 
221
- def pretty_key(k):
222
- return SHORT_NAMES.get(k, k[:12])
223
 
224
 
225
  # ==============================
226
  # Classifiers
227
  # ==============================
228
- def classify_price_volume_subgroup(key):
229
- k = key.lower()
230
- if "volume" in k: return "Volume"
231
- if "average" in k or "dma" in k: return "Moving Avg"
232
- if "week" in k or "beta" in k: return "Range / Vol"
233
- if "target" in k or "recommend" in k: return "Bid / Analyst"
234
- return "Live Price"
235
-
236
-
237
- def classify_key(key, value):
238
- k = key.lower()
239
- if isinstance(value,(int,float)) and any(x in k for x in [
240
- "price","volume","avg","change","percent","market","week","beta","target"
241
- ]):
242
  return "price_volume"
243
- if any(x in k for x in [
244
- "revenue","income","profit","margin","pe","pb","roe","roa","debt","equity"
245
- ]):
246
- return "fundamental"
247
- if isinstance(value,str) and len(value) > 80:
248
  return "long_text"
249
  return "profile"
250
 
@@ -253,62 +197,29 @@ def classify_key(key, value):
253
  # Group builder
254
  # ==============================
255
  def build_grouped_info(info):
256
- groups = {
257
- "price_volume":{},
258
- "fundamental":{},
259
- "profile":{},
260
- "long_text":{}
261
- }
262
  for k,v in info.items():
263
- if v in [None,"",[],{}]:
264
- continue
265
- groups[classify_key(k,v)][k] = v
266
- return groups
267
-
268
-
269
- # ==============================
270
- # Column splitter
271
- # ==============================
272
- def split_df_evenly(df):
273
- if df is None or df.empty:
274
- return []
275
- n = len(df)
276
- cols = 1 if n <= 6 else 2 if n <= 14 else 3
277
- chunk = (n + cols - 1) // cols
278
- return [df.iloc[i:i+chunk] for i in range(0, n, chunk)]
279
 
280
 
281
  # ==============================
282
- # DataFrame builder (SORT BY DISPLAY NAME)
283
  # ==============================
284
  def build_df_from_dict(data):
285
  rows = []
286
-
287
- for k, v in data.items():
288
- if is_noise(k):
289
- continue
290
-
291
- label = pretty_key(k) # 🔑 renamed key
292
-
293
- if isinstance(v, (int, float)):
294
- v = format_number(v)
295
-
296
- rows.append((label, v))
297
-
298
- # 🔑 SORT BY DISPLAY NAME
299
  rows.sort(key=lambda x: x[0].lower())
300
-
301
- return pd.DataFrame(rows, columns=["Field", "Value"])
302
 
303
 
304
  # ==============================
305
- # MAIN FUNCTION (CACHED)
306
  # ==============================
307
  def fetch_info(symbol):
308
- """
309
- Cached Yahoo Finance info renderer
310
- Cache validity: 1 hour
311
- """
312
  key = f"info_{symbol}"
313
 
314
  if exists(key, "html"):
@@ -318,66 +229,44 @@ def fetch_info(symbol):
318
 
319
  try:
320
  info = yfinfo(symbol)
321
- if not info or "__error__" in info:
322
  return "No data"
323
 
324
  groups = build_grouped_info(info)
325
  html = ""
326
 
327
- # PRICE / VOLUME
328
- pv = resolve_duplicates(groups["price_volume"])
329
- sub = {}
330
- for k,v in pv.items():
331
- sg = classify_price_volume_subgroup(k)
332
- sub.setdefault(sg,{})[k] = v
333
-
334
- cards = ""
335
- for i,(t,d) in enumerate(sub.items()):
336
- df = build_df_from_dict(d)
337
- if not df.empty:
338
- cards += html_card(
339
- f"{SUBGROUP_ICONS.get(t,'ℹ️')} {t}",
340
- make_table(df),
341
- mini=True,
342
- shade=i
343
- )
344
-
345
- if cards:
346
  html += html_card(
347
  f"{MAIN_ICONS['Price / Volume']} Price / Volume",
348
- column_layout(cards),
349
- shade=0
350
  )
351
 
352
- # FUNDAMENTALS
353
- if groups["fundamental"]:
354
- chunks = split_df_evenly(build_df_from_dict(groups["fundamental"]))
355
- cols = "".join(
356
- html_card("📊 Fundamentals", make_table(c), mini=True, shade=i)
357
- for i,c in enumerate(chunks)
358
- )
359
  html += html_card(
360
- f"{MAIN_ICONS['Fundamentals']} Fundamentals",
361
- column_layout(cols),
362
- shade=1
363
  )
364
 
365
- # PROFILE
366
- if groups["profile"]:
367
- chunks = split_df_evenly(build_df_from_dict(groups["profile"]))
368
- cols = "".join(
369
- html_card("🏢 Profile", make_table(c), mini=True, shade=i)
370
- for i,c in enumerate(chunks)
371
- )
 
 
 
372
  html += html_card(
373
- f"{MAIN_ICONS['Company Profile']} Company Profile",
374
- column_layout(cols),
375
- shade=2
376
  )
377
 
378
- # LONG TEXT
379
  for k,v in groups["long_text"].items():
380
- html += html_card(pretty_key(k), v, shade=2)
381
 
382
  if html.strip():
383
  save(key, html, "html")
 
4
  import yfinance as yf
5
  import pandas as pd
6
  import traceback
 
 
7
  from datetime import datetime, timezone
8
 
9
  # persist helpers
 
14
  # Yahoo Finance info fetch (RAW)
15
  # ==============================
16
  def yfinfo(symbol):
 
 
 
 
17
  try:
18
  t = yf.Ticker(symbol + ".NS")
19
  info = t.info
20
+ return info if isinstance(info, dict) else {}
 
 
21
  except Exception as e:
22
  return {"__error__": str(e)}
23
 
 
37
  MAIN_ICONS = {
38
  "Price / Volume": "📈",
39
  "Fundamentals": "📊",
40
+ "Company Profile": "🏢",
41
+ "Management": "👔"
42
  }
43
 
44
 
45
  # ==============================
46
+ # Layout helpers
47
  # ==============================
48
  def column_layout(html, min_width=320):
49
  return f"""
50
+ <div style="display:grid;
51
+ grid-template-columns:repeat(auto-fit,minmax({min_width}px,1fr));
52
+ gap:10px;">
 
 
 
53
  {html}
54
  </div>
55
  """
56
 
57
 
 
 
 
58
  def html_card(title, body, mini=False, shade=0):
59
  font = "12px" if mini else "14px"
60
  pad = "6px" if mini else "10px"
 
67
  ]
68
 
69
  return f"""
70
+ <div style="background:{shades[shade%3]};
71
+ border:1px solid #a3c0e0;
72
+ border-radius:8px;
73
+ padding:{pad};
74
+ font-size:{font};">
75
+ <div style="background:{grads[shade%3]};
76
+ color:white;
77
+ padding:4px 8px;
78
+ border-radius:6px;
79
+ font-weight:600;
80
+ margin-bottom:6px;">
 
 
 
 
 
81
  {title}
82
  </div>
83
  {body}
 
88
  # ==============================
89
  # Formatting helpers
90
  # ==============================
91
+ def human_number(n):
92
+ try:
93
+ n = float(n)
94
+ abs_n = abs(n)
95
+ if abs_n >= 1e7:
96
+ return f"{n/1e7:.2f}Cr"
97
+ if abs_n >= 1e5:
98
+ return f"{n/1e5:.2f}L"
99
+ if abs_n >= 1e3:
100
+ return f"{n/1e3:.2f}K"
101
+ return f"{n:,.2f}"
102
+ except:
103
+ return str(n)
104
+
105
+
106
+ def format_date(v):
107
  try:
108
+ if isinstance(v, (int, float)):
109
+ return datetime.fromtimestamp(v, tz=timezone.utc).strftime("%d %b %Y")
110
+ if isinstance(v, str) and v.isdigit():
111
+ return datetime.fromtimestamp(int(v), tz=timezone.utc).strftime("%d %b %Y")
112
+ return v
 
113
  except:
114
+ return v
115
+
116
+
117
+ def format_value(k, v):
118
+ lk = k.lower()
119
+
120
+ if isinstance(v, (int, float)):
121
+ if "percent" in lk or "yield" in lk:
122
+ return f"{v:.2f}%"
123
+ if "marketcap" in lk or "revenue" in lk or "income" in lk:
124
+ return "₹" + human_number(v)
125
+ return human_number(v)
126
+
127
+ if "date" in lk or "time" in lk:
128
+ return format_date(v)
129
+
130
+ return v
131
 
132
 
133
  # ==============================
134
+ # Compact table
135
  # ==============================
136
  def make_table(df):
137
+ return "".join(
138
+ f"""
139
+ <div style="display:flex;justify-content:space-between;
140
+ border-bottom:1px dashed #bcd0ea;padding:2px 0;">
141
+ <span style="color:#1a4f8a;">{r.Field}</span>
142
+ <span style="font-weight:600;">{r.Value}</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
  </div>
144
  """
145
+ for r in df.itertuples()
146
+ )
147
 
148
 
149
  # ==============================
150
+ # Noise
151
  # ==============================
152
  NOISE_KEYS = {
153
  "maxAge","priceHint","triggerable",
 
156
  "esgPopulated"
157
  }
158
 
159
+ def is_noise(k): return k in NOISE_KEYS
 
160
 
161
 
162
  # ==============================
163
+ # Short display names
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
164
  # ==============================
165
  SHORT_NAMES = {
166
  "regularMarketPrice":"Price",
 
171
  "regularMarketDayHigh":"High",
172
  "regularMarketDayLow":"Low",
173
  "regularMarketVolume":"Vol",
174
+ "marketCap":"MCap",
 
 
 
 
 
175
  "beta":"Beta",
176
  "targetMeanPrice":"Target"
177
  }
178
 
179
+ def pretty_key(k): return SHORT_NAMES.get(k, k[:14])
 
180
 
181
 
182
  # ==============================
183
  # Classifiers
184
  # ==============================
185
+ def classify_key(k, v):
186
+ lk = k.lower()
187
+ if k == "companyOfficers":
188
+ return "management"
189
+ if isinstance(v,(int,float)):
 
 
 
 
 
 
 
 
 
190
  return "price_volume"
191
+ if isinstance(v,str) and len(v) > 80:
 
 
 
 
192
  return "long_text"
193
  return "profile"
194
 
 
197
  # Group builder
198
  # ==============================
199
  def build_grouped_info(info):
200
+ g = {"price_volume":{}, "profile":{}, "long_text":{}, "management":{}}
 
 
 
 
 
201
  for k,v in info.items():
202
+ if v in [None,"",[],{}]: continue
203
+ g[classify_key(k,v)][k] = v
204
+ return g
 
 
 
 
 
 
 
 
 
 
 
 
 
205
 
206
 
207
  # ==============================
208
+ # DF builder (sorted by display name)
209
  # ==============================
210
  def build_df_from_dict(data):
211
  rows = []
212
+ for k,v in data.items():
213
+ if is_noise(k): continue
214
+ rows.append((pretty_key(k), format_value(k,v)))
 
 
 
 
 
 
 
 
 
 
215
  rows.sort(key=lambda x: x[0].lower())
216
+ return pd.DataFrame(rows, columns=["Field","Value"])
 
217
 
218
 
219
  # ==============================
220
+ # MAIN
221
  # ==============================
222
  def fetch_info(symbol):
 
 
 
 
223
  key = f"info_{symbol}"
224
 
225
  if exists(key, "html"):
 
229
 
230
  try:
231
  info = yfinfo(symbol)
232
+ if "__error__" in info:
233
  return "No data"
234
 
235
  groups = build_grouped_info(info)
236
  html = ""
237
 
238
+ # Price / Volume
239
+ if groups["price_volume"]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
240
  html += html_card(
241
  f"{MAIN_ICONS['Price / Volume']} Price / Volume",
242
+ make_table(build_df_from_dict(groups["price_volume"]))
 
243
  )
244
 
245
+ # Profile
246
+ if groups["profile"]:
 
 
 
 
 
247
  html += html_card(
248
+ f"{MAIN_ICONS['Company Profile']} Company Profile",
249
+ make_table(build_df_from_dict(groups["profile"]))
 
250
  )
251
 
252
+ # Management (companyOfficers)
253
+ if groups["management"].get("companyOfficers"):
254
+ officers = ""
255
+ for o in groups["management"]["companyOfficers"]:
256
+ officers += html_card(
257
+ o.get("name",""),
258
+ f"{o.get('title','')}<br/>Pay: ₹{human_number(o.get('totalPay',0))}",
259
+ mini=True
260
+ )
261
+
262
  html += html_card(
263
+ f"{MAIN_ICONS['Management']} Management",
264
+ column_layout(officers)
 
265
  )
266
 
267
+ # Long text
268
  for k,v in groups["long_text"].items():
269
+ html += html_card(pretty_key(k), v)
270
 
271
  if html.strip():
272
  save(key, html, "html")