eshan6704 commited on
Commit
8249acf
·
verified ·
1 Parent(s): bd45e82

Update app/yahooinfo.py

Browse files
Files changed (1) hide show
  1. app/yahooinfo.py +296 -215
app/yahooinfo.py CHANGED
@@ -1,136 +1,168 @@
1
- # ======================================================
2
- # FINAL YAHOO FINANCE INFO PROCESSOR + RENDERER
3
- # ======================================================
4
-
5
  import yfinance as yf
6
  import pandas as pd
7
  import traceback
8
- import re
9
- from datetime import datetime, UTC
10
- from math import floor
11
-
12
- from .persist import exists, load, save
13
-
14
 
15
- # ======================================================
16
- # CONSTANTS
17
- # ======================================================
18
- CR = 10_000_000
19
- KCR = 10_000_000_000
20
- LCR = 10_000_000_000_000
21
 
22
- DATE_KEYS = ("date", "timestamp", "time", "yearend")
 
23
 
24
 
25
- # ======================================================
26
- # RAW YAHOO FETCH
27
- # ======================================================
28
  def yfinfo(symbol):
 
 
 
 
29
  try:
30
  t = yf.Ticker(symbol + ".NS")
31
  info = t.info
32
- return info if isinstance(info, dict) else {}
 
 
33
  except Exception as e:
34
  return {"__error__": str(e)}
35
 
36
 
37
- # ======================================================
38
- # HELPERS
39
- # ======================================================
40
- def truncate(num, decimals=4):
41
- f = 10 ** decimals
42
- return floor(num * f) / f
43
-
44
-
45
- def compress_key(key, used):
46
- if len(key) <= 10:
47
- used.add(key)
48
- return key
49
-
50
- parts = re.findall(r"[A-Z][a-z]*|[a-z]+", key)
51
- base = "".join(p[:3] for p in parts)
52
-
53
- if base not in used:
54
- used.add(base)
55
- return base
56
-
57
- i = 1
58
- while f"{base}_{i}" in used:
59
- i += 1
60
-
61
- final = f"{base}_{i}"
62
- used.add(final)
63
- return final
64
-
65
-
66
- # ======================================================
67
- # CORE PROCESSOR
68
- # ======================================================
69
- def process_info(info):
70
- main = {}
71
- long_text = {}
72
-
73
- new_to_old = {}
74
- old_to_new = {}
75
 
76
- used_keys = set()
 
 
 
 
77
 
78
- for old_k, v in info.items():
79
- lk = old_k.lower()
80
- new_k = compress_key(old_k, used_keys)
81
 
82
- new_to_old[new_k] = old_k
83
- old_to_new[old_k] = new_k
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
 
85
- # ---- Long text ----
86
- if isinstance(v, str) and len(v) > 200:
87
- long_text[new_k] = v
88
- continue
89
 
90
- # ---- Date conversion ----
91
- if (
92
- isinstance(v, int)
93
- and v >= 1_000_000_000
94
- and any(x in lk for x in DATE_KEYS)
95
- ):
96
- if v > 1_000_000_000_000:
97
- v //= 1000
98
  try:
99
- main[new_k] = datetime.fromtimestamp(v, UTC).strftime("%d-%m-%Y")
100
  except:
101
- main[new_k] = v
102
- continue
103
-
104
- # ---- Large numbers (Cr/KCr/LCr) ----
105
- if isinstance(v, (int, float)) and abs(v) >= CR:
106
- av = abs(v)
107
- sign = -1 if v < 0 else 1
108
-
109
- if av >= LCR:
110
- main[new_k] = f"{sign * av / LCR:.2f} LCr"
111
- elif av >= KCR:
112
- main[new_k] = f"{sign * av / KCR:.2f} KCr"
113
- else:
114
- main[new_k] = f"{sign * av / CR:.2f} Cr"
115
- continue
116
-
117
- # ---- Float trimming ----
118
- if isinstance(v, float):
119
- main[new_k] = 0.0 if v == 0.0 else truncate(v)
120
- continue
121
-
122
- main[new_k] = v
123
-
124
- return main, long_text, new_to_old, old_to_new
125
-
126
-
127
- # ======================================================
128
- # NOISE
129
- # ======================================================
 
130
  NOISE_KEYS = {
131
- "maxAge", "priceHint", "triggerable",
132
  "customPriceAlertConfidence",
133
- "sourceInterval", "exchangeDataDelayedBy",
134
  "esgPopulated"
135
  }
136
 
@@ -138,45 +170,63 @@ def is_noise(k):
138
  return k in NOISE_KEYS
139
 
140
 
141
- # ======================================================
142
- # DUPLICATE RESOLUTION (OLD KEYS)
143
- # ======================================================
144
  DUPLICATE_PRIORITY = {
145
- "price": ["regularMarketPrice", "currentPrice"],
146
- "prev": ["regularMarketPreviousClose", "previousClose"],
147
- "open": ["regularMarketOpen", "open"],
148
- "high": ["regularMarketDayHigh", "dayHigh"],
149
- "low": ["regularMarketDayLow", "dayLow"],
150
- "volume": ["regularMarketVolume", "volume"],
151
  }
152
 
153
- def resolve_duplicates(data, new_to_old):
154
- resolved = {}
155
- used_old = set()
156
-
157
  for keys in DUPLICATE_PRIORITY.values():
158
- for old_k in keys:
159
- for new_k, mapped_old in new_to_old.items():
160
- if mapped_old == old_k and new_k in data:
161
- resolved[new_k] = data[new_k]
162
- used_old.update(keys)
163
- break
164
- else:
165
- continue
166
- break
167
-
168
- for new_k, v in data.items():
169
- if new_to_old[new_k] not in used_old:
170
- resolved[new_k] = v
171
-
172
  return resolved
173
 
174
 
175
- # ======================================================
176
- # CLASSIFIERS (UNCHANGED – OLD KEYS ONLY)
177
- # ======================================================
178
- def classify_price_volume_subgroup(old_key):
179
- k = old_key.lower()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
  if "volume" in k: return "Volume"
181
  if "average" in k or "dma" in k: return "Moving Avg"
182
  if "week" in k or "beta" in k: return "Range / Vol"
@@ -184,114 +234,145 @@ def classify_price_volume_subgroup(old_key):
184
  return "Live Price"
185
 
186
 
187
- def classify_key(old_key, value):
188
- k = old_key.lower()
189
-
190
- if isinstance(value, (int, float)) and any(x in k for x in [
191
- "price", "volume", "avg", "change", "percent",
192
- "market", "week", "beta", "target"
193
  ]):
194
  return "price_volume"
195
-
196
  if any(x in k for x in [
197
- "revenue", "income", "profit", "margin",
198
- "pe", "pb", "roe", "roa", "debt", "equity"
199
  ]):
200
  return "fundamental"
201
-
202
- if isinstance(value, str) and len(value) > 80:
203
  return "long_text"
204
-
205
  return "profile"
206
 
207
 
208
- # ======================================================
209
- # GROUP BUILDER (KEY POINT)
210
- # ======================================================
211
- def build_grouped_info(info, new_to_old):
212
  groups = {
213
- "price_volume": {},
214
- "fundamental": {},
215
- "profile": {},
216
- "long_text": {},
217
  }
218
-
219
- for new_k, v in info.items():
220
- if v in [None, "", [], {}]:
221
  continue
222
-
223
- old_k = new_to_old.get(new_k, new_k)
224
- g = classify_key(old_k, v)
225
- groups[g][new_k] = v
226
-
227
  return groups
228
 
229
 
230
- # ======================================================
231
- # FORMATTERS
232
- # ======================================================
233
- def format_number(x):
234
- try:
235
- x = float(x)
236
- if abs(x) >= 100:
237
- return f"{x:,.0f}"
238
- if abs(x) >= 1:
239
- return f"{x:,.2f}"
240
- return f"{x:.4f}"
241
- except:
242
- return str(x)
243
 
244
 
 
 
 
245
  def build_df_from_dict(data):
246
  rows = []
247
- for k, v in data.items():
248
  if is_noise(k):
249
  continue
250
- if isinstance(v, (int, float)):
251
  v = format_number(v)
252
- rows.append([k, v])
253
- return pd.DataFrame(rows, columns=["Field", "Value"])
254
 
255
 
256
- # ======================================================
257
- # MAIN ENTRY (CACHED)
258
- # ======================================================
259
  def fetch_info(symbol):
260
- cache_key = f"info_{symbol}"
261
-
262
- if exists(cache_key, "html"):
263
- cached = load(cache_key, "html")
 
 
 
 
264
  if cached:
265
  return cached
266
 
267
  try:
268
- raw = yfinfo(symbol)
269
- if not raw or "__error__" in raw:
270
  return "No data"
271
 
272
- info, long_text, new_to_old, _ = process_info(raw)
273
- groups = build_grouped_info(info, new_to_old)
274
-
275
  html = ""
276
 
277
- # ---- PRICE / VOLUME ----
278
- pv = resolve_duplicates(groups["price_volume"], new_to_old)
279
- for new_k, v in pv.items():
280
- html += f"<div><b>{new_k}</b> : {v}</div>"
281
-
282
- # ---- FUNDAMENTALS ----
283
- for new_k, v in groups["fundamental"].items():
284
- html += f"<div><b>{new_k}</b> : {v}</div>"
285
-
286
- # ---- PROFILE ----
287
- for new_k, v in groups["profile"].items():
288
- html += f"<div><b>{new_k}</b> : {v}</div>"
289
-
290
- # ---- LONG TEXT ----
291
- for v in long_text.values():
292
- html += f"<div style='margin-top:8px'>{v}</div>"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
293
 
294
- save(cache_key, html, "html")
295
  return html
296
 
297
  except Exception:
 
1
+ # ==============================
2
+ # Imports
3
+ # ==============================
 
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
12
+ from .persist import exists, load, save
13
 
14
 
15
+ # ==============================
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
 
32
 
33
+ # ==============================
34
+ # Icons
35
+ # ==============================
36
+ SUBGROUP_ICONS = {
37
+ "Live Price": "💹",
38
+ "Volume": "📊",
39
+ "Moving Avg": "📈",
40
+ "Range / Vol": "📉",
41
+ "Bid / Analyst": "📝",
42
+ "Other": "ℹ️"
43
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
 
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"
74
+
75
+ shades = ["#e6f0fa", "#d7e3f5", "#c8d6f0"]
76
+ grads = [
77
+ "linear-gradient(to right,#1a4f8a,#4a7ac7)",
78
+ "linear-gradient(to right,#1f5595,#5584d6)",
79
+ "linear-gradient(to right,#205ca0,#6192e0)"
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}
102
+ </div>
103
+ """
104
+
105
+
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",
164
  "customPriceAlertConfidence",
165
+ "sourceInterval","exchangeDataDelayedBy",
166
  "esgPopulated"
167
  }
168
 
 
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
201
+ # ==============================
202
+ SHORT_NAMES = {
203
+ "regularMarketPrice":"Price",
204
+ "regularMarketChange":"Chg",
205
+ "regularMarketChangePercent":"Chg%",
206
+ "regularMarketPreviousClose":"Prev",
207
+ "regularMarketOpen":"Open",
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"
 
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
 
251
 
252
+ # ==============================
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
283
+ # ==============================
284
  def build_df_from_dict(data):
285
  rows = []
286
+ for k,v in data.items():
287
  if is_noise(k):
288
  continue
289
+ if isinstance(v,(int,float)):
290
  v = format_number(v)
291
+ rows.append([pretty_key(k), v])
292
+ return pd.DataFrame(rows, columns=["Field","Value"])
293
 
294
 
295
+ # ==============================
296
+ # MAIN FUNCTION (CACHED)
297
+ # ==============================
298
  def fetch_info(symbol):
299
+ """
300
+ Cached Yahoo Finance info renderer
301
+ Cache validity: 1 hour
302
+ """
303
+ key = f"info_{symbol}"
304
+
305
+ if exists(key, "html"):
306
+ cached = load(key, "html")
307
  if cached:
308
  return cached
309
 
310
  try:
311
+ info = yfinfo(symbol)
312
+ if not info or "__error__" in info:
313
  return "No data"
314
 
315
+ groups = build_grouped_info(info)
 
 
316
  html = ""
317
 
318
+ # PRICE / VOLUME
319
+ pv = resolve_duplicates(groups["price_volume"])
320
+ sub = {}
321
+ for k,v in pv.items():
322
+ sg = classify_price_volume_subgroup(k)
323
+ sub.setdefault(sg,{})[k] = v
324
+
325
+ cards = ""
326
+ for i,(t,d) in enumerate(sub.items()):
327
+ df = build_df_from_dict(d)
328
+ if not df.empty:
329
+ cards += html_card(
330
+ f"{SUBGROUP_ICONS.get(t,'ℹ️')} {t}",
331
+ make_table(df),
332
+ mini=True,
333
+ shade=i
334
+ )
335
+
336
+ if cards:
337
+ html += html_card(
338
+ f"{MAIN_ICONS['Price / Volume']} Price / Volume",
339
+ column_layout(cards),
340
+ shade=0
341
+ )
342
+
343
+ # FUNDAMENTALS
344
+ if groups["fundamental"]:
345
+ chunks = split_df_evenly(build_df_from_dict(groups["fundamental"]))
346
+ cols = "".join(
347
+ html_card("📊 Fundamentals", make_table(c), mini=True, shade=i)
348
+ for i,c in enumerate(chunks)
349
+ )
350
+ html += html_card(
351
+ f"{MAIN_ICONS['Fundamentals']} Fundamentals",
352
+ column_layout(cols),
353
+ shade=1
354
+ )
355
+
356
+ # PROFILE
357
+ if groups["profile"]:
358
+ chunks = split_df_evenly(build_df_from_dict(groups["profile"]))
359
+ cols = "".join(
360
+ html_card("🏢 Profile", make_table(c), mini=True, shade=i)
361
+ for i,c in enumerate(chunks)
362
+ )
363
+ html += html_card(
364
+ f"{MAIN_ICONS['Company Profile']} Company Profile",
365
+ column_layout(cols),
366
+ shade=2
367
+ )
368
+
369
+ # LONG TEXT
370
+ for k,v in groups["long_text"].items():
371
+ html += html_card(pretty_key(k), v, shade=2)
372
+
373
+ if html.strip():
374
+ save(key, html, "html")
375
 
 
376
  return html
377
 
378
  except Exception: