eshan6704 commited on
Commit
9083cfd
Β·
verified Β·
1 Parent(s): f376ad9

Update app/yahooinfo.py

Browse files
Files changed (1) hide show
  1. app/yahooinfo.py +113 -71
app/yahooinfo.py CHANGED
@@ -6,12 +6,11 @@ import pandas as pd
6
  import traceback
7
  from datetime import datetime, timezone
8
 
9
- # persist helpers
10
  from .persist import exists, load, save
11
 
12
 
13
  # ==============================
14
- # Yahoo Finance info fetch (RAW)
15
  # ==============================
16
  def yfinfo(symbol):
17
  try:
@@ -25,33 +24,45 @@ def yfinfo(symbol):
25
  # ==============================
26
  # Icons
27
  # ==============================
28
- SUBGROUP_ICONS = {
29
- "Live Price": "πŸ’Ή",
30
- "Volume": "πŸ“Š",
31
- "Moving Avg": "πŸ“ˆ",
32
- "Range / Vol": "πŸ“‰",
33
- "Bid / Analyst": "πŸ“",
34
- "Other": "ℹ️"
35
- }
36
-
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
 
@@ -91,13 +102,9 @@ def html_card(title, body, mini=False, shade=0):
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)
@@ -107,8 +114,6 @@ 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
@@ -116,13 +121,18 @@ def format_date(v):
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)
@@ -131,7 +141,7 @@ def format_value(k, v):
131
 
132
 
133
  # ==============================
134
- # Compact table
135
  # ==============================
136
  def make_table(df):
137
  return "".join(
@@ -139,7 +149,7 @@ def make_table(df):
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()
@@ -147,7 +157,7 @@ def make_table(df):
147
 
148
 
149
  # ==============================
150
- # Noise
151
  # ==============================
152
  NOISE_KEYS = {
153
  "maxAge","priceHint","triggerable",
@@ -159,9 +169,6 @@ NOISE_KEYS = {
159
  def is_noise(k): return k in NOISE_KEYS
160
 
161
 
162
- # ==============================
163
- # Short display names
164
- # ==============================
165
  SHORT_NAMES = {
166
  "regularMarketPrice":"Price",
167
  "regularMarketChange":"Chg",
@@ -176,43 +183,69 @@ SHORT_NAMES = {
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
 
195
 
196
- # ==============================
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
 
@@ -232,41 +265,50 @@ def fetch_info(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")
 
6
  import traceback
7
  from datetime import datetime, timezone
8
 
 
9
  from .persist import exists, load, save
10
 
11
 
12
  # ==============================
13
+ # Yahoo Finance info fetch
14
  # ==============================
15
  def yfinfo(symbol):
16
  try:
 
24
  # ==============================
25
  # Icons
26
  # ==============================
 
 
 
 
 
 
 
 
 
27
  MAIN_ICONS = {
28
  "Price / Volume": "πŸ“ˆ",
 
29
  "Company Profile": "🏒",
30
  "Management": "πŸ‘”"
31
  }
32
 
33
 
34
  # ==============================
35
+ # Responsive layout
36
  # ==============================
37
+ def column_layout(html):
38
  return f"""
39
+ <style>
40
+ .grid {{
41
+ display:grid;
42
+ gap:10px;
43
+ grid-template-columns:repeat(3,1fr);
44
+ }}
45
+ @media(max-width:1024px) {{
46
+ .grid {{ grid-template-columns:repeat(2,1fr); }}
47
+ }}
48
+ @media(max-width:640px) {{
49
+ .grid {{ grid-template-columns:1fr; }}
50
+ }}
51
+ .pos {{ color:#0a7d32; font-weight:600; }}
52
+ .neg {{ color:#b00020; font-weight:600; }}
53
+ </style>
54
+ <div class="grid">{html}</div>
55
+ """
56
+
57
+
58
+ def collapsible(title, body):
59
+ return f"""
60
+ <details open>
61
+ <summary style="cursor:pointer;font-weight:600;font-size:15px;padding:6px 0;">
62
+ {title}
63
+ </summary>
64
+ {body}
65
+ </details>
66
  """
67
 
68
 
 
102
  def human_number(n):
103
  try:
104
  n = float(n)
105
+ if abs(n) >= 1e7: return f"{n/1e7:.2f}Cr"
106
+ if abs(n) >= 1e5: return f"{n/1e5:.2f}L"
107
+ if abs(n) >= 1e3: return f"{n/1e3:.2f}K"
 
 
 
 
108
  return f"{n:,.2f}"
109
  except:
110
  return str(n)
 
114
  try:
115
  if isinstance(v, (int, float)):
116
  return datetime.fromtimestamp(v, tz=timezone.utc).strftime("%d %b %Y")
 
 
117
  return v
118
  except:
119
  return v
 
121
 
122
  def format_value(k, v):
123
  lk = k.lower()
124
+ arrow = ""
125
+ cls = ""
126
 
127
  if isinstance(v, (int, float)):
128
+ if v > 0: cls, arrow = "pos", "↑"
129
+ elif v < 0: cls, arrow = "neg", "↓"
130
+
131
+ if "percent" in lk:
132
+ return f'<span class="{cls}">{arrow}{v:.2f}%</span>'
133
+ if "marketcap" in lk:
134
+ return f'<span class="{cls}">β‚Ή{human_number(v)}</span>'
135
+ return f'<span class="{cls}">{human_number(v)}</span>'
136
 
137
  if "date" in lk or "time" in lk:
138
  return format_date(v)
 
141
 
142
 
143
  # ==============================
144
+ # Table renderer
145
  # ==============================
146
  def make_table(df):
147
  return "".join(
 
149
  <div style="display:flex;justify-content:space-between;
150
  border-bottom:1px dashed #bcd0ea;padding:2px 0;">
151
  <span style="color:#1a4f8a;">{r.Field}</span>
152
+ <span>{r.Value}</span>
153
  </div>
154
  """
155
  for r in df.itertuples()
 
157
 
158
 
159
  # ==============================
160
+ # Utils
161
  # ==============================
162
  NOISE_KEYS = {
163
  "maxAge","priceHint","triggerable",
 
169
  def is_noise(k): return k in NOISE_KEYS
170
 
171
 
 
 
 
172
  SHORT_NAMES = {
173
  "regularMarketPrice":"Price",
174
  "regularMarketChange":"Chg",
 
183
  "targetMeanPrice":"Target"
184
  }
185
 
186
+ # πŸ”‘ USER CONFIGURABLE
187
+ PINNED_FIELDS = [
188
+ "Price","Chg","Chg%","Open","High","Low","Vol","MCap","Beta"
189
+ ]
190
+
191
  def pretty_key(k): return SHORT_NAMES.get(k, k[:14])
192
 
193
 
194
  # ==============================
195
+ # Grouping
196
  # ==============================
197
  def classify_key(k, v):
 
198
  if k == "companyOfficers":
199
  return "management"
200
+ if isinstance(v, (int, float)):
201
  return "price_volume"
202
+ if isinstance(v, str) and len(v) > 80:
203
  return "long_text"
204
  return "profile"
205
 
206
 
 
 
 
207
  def build_grouped_info(info):
208
+ g = {
209
+ "price_volume": {},
210
+ "profile": {},
211
+ "management": {},
212
+ "long_text": {}
213
+ }
214
+ for k, v in info.items():
215
+ if v in [None, "", [], {}]:
216
+ continue
217
+ g[classify_key(k, v)][k] = v
218
  return g
219
 
220
 
221
  # ==============================
222
+ # Column splitter
223
+ # ==============================
224
+ def split_df_evenly(df):
225
+ if df.empty: return []
226
+ n = len(df)
227
+ cols = 1 if n <= 6 else 2 if n <= 14 else 3
228
+ chunk = (n + cols - 1) // cols
229
+ return [df.iloc[i:i+chunk] for i in range(0, n, chunk)]
230
+
231
+
232
+ # ==============================
233
+ # DF builder (PIN + SORT)
234
  # ==============================
235
  def build_df_from_dict(data):
236
  rows = []
237
+ for k, v in data.items():
238
  if is_noise(k): continue
239
+ label = pretty_key(k)
240
+ rows.append((label, format_value(k, v)))
241
+
242
+ rows.sort(
243
+ key=lambda x: (
244
+ 0 if x[0] in PINNED_FIELDS else 1,
245
+ PINNED_FIELDS.index(x[0]) if x[0] in PINNED_FIELDS else x[0].lower()
246
+ )
247
+ )
248
+
249
  return pd.DataFrame(rows, columns=["Field","Value"])
250
 
251
 
 
265
  if "__error__" in info:
266
  return "No data"
267
 
268
+ g = build_grouped_info(info)
269
  html = ""
270
 
271
+ # PRICE / VOLUME
272
+ if g["price_volume"]:
273
+ df = build_df_from_dict(g["price_volume"])
274
+ cols = "".join(
275
+ html_card("πŸ“ˆ Price & Volume", make_table(c), mini=True, shade=i)
276
+ for i,c in enumerate(split_df_evenly(df))
277
+ )
278
+ html += collapsible(
279
  f"{MAIN_ICONS['Price / Volume']} Price / Volume",
280
+ column_layout(cols)
281
  )
282
 
283
+ # COMPANY PROFILE
284
+ if g["profile"]:
285
+ df = build_df_from_dict(g["profile"])
286
+ cols = "".join(
287
+ html_card("🏒 Profile", make_table(c), mini=True, shade=i)
288
+ for i,c in enumerate(split_df_evenly(df))
289
+ )
290
+ html += collapsible(
291
  f"{MAIN_ICONS['Company Profile']} Company Profile",
292
+ column_layout(cols)
293
  )
294
 
295
+ # MANAGEMENT
296
+ if g["management"].get("companyOfficers"):
297
  officers = ""
298
+ for o in g["management"]["companyOfficers"]:
299
  officers += html_card(
300
  o.get("name",""),
301
  f"{o.get('title','')}<br/>Pay: β‚Ή{human_number(o.get('totalPay',0))}",
302
  mini=True
303
  )
304
+ html += collapsible(
 
305
  f"{MAIN_ICONS['Management']} Management",
306
  column_layout(officers)
307
  )
308
 
309
+ # LONG TEXT
310
+ for k,v in g["long_text"].items():
311
+ html += collapsible(pretty_key(k), v)
312
 
313
  if html.strip():
314
  save(key, html, "html")