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

Update app/yahooinfo.py

Browse files
Files changed (1) hide show
  1. app/yahooinfo.py +128 -102
app/yahooinfo.py CHANGED
@@ -10,7 +10,7 @@ from .persist import exists, load, save
10
 
11
 
12
  # ==============================
13
- # Yahoo Finance info fetch
14
  # ==============================
15
  def yfinfo(symbol):
16
  try:
@@ -26,13 +26,17 @@ def yfinfo(symbol):
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"""
@@ -48,8 +52,8 @@ def column_layout(html):
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
  """
@@ -69,13 +73,7 @@ def collapsible(title, body):
69
  def html_card(title, body, mini=False, shade=0):
70
  font = "12px" if mini else "14px"
71
  pad = "6px" if mini else "10px"
72
-
73
- shades = ["#e6f0fa", "#d7e3f5", "#c8d6f0"]
74
- grads = [
75
- "linear-gradient(to right,#1a4f8a,#4a7ac7)",
76
- "linear-gradient(to right,#1f5595,#5584d6)",
77
- "linear-gradient(to right,#205ca0,#6192e0)"
78
- ]
79
 
80
  return f"""
81
  <div style="background:{shades[shade%3]};
@@ -83,12 +81,7 @@ def html_card(title, body, mini=False, shade=0):
83
  border-radius:8px;
84
  padding:{pad};
85
  font-size:{font};">
86
- <div style="background:{grads[shade%3]};
87
- color:white;
88
- padding:4px 8px;
89
- border-radius:6px;
90
- font-weight:600;
91
- margin-bottom:6px;">
92
  {title}
93
  </div>
94
  {body}
@@ -97,7 +90,7 @@ def html_card(title, body, mini=False, shade=0):
97
 
98
 
99
  # ==============================
100
- # Formatting helpers
101
  # ==============================
102
  def human_number(n):
103
  try:
@@ -112,25 +105,19 @@ def human_number(n):
112
 
113
  def format_date(v):
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
120
 
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
 
@@ -140,15 +127,12 @@ def format_value(k, v):
140
  return v
141
 
142
 
143
- # ==============================
144
- # Table renderer
145
- # ==============================
146
  def make_table(df):
147
  return "".join(
148
  f"""
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
  """
@@ -157,7 +141,7 @@ def make_table(df):
157
 
158
 
159
  # ==============================
160
- # Utils
161
  # ==============================
162
  NOISE_KEYS = {
163
  "maxAge","priceHint","triggerable",
@@ -166,37 +150,40 @@ NOISE_KEYS = {
166
  "esgPopulated"
167
  }
168
 
169
- def is_noise(k): return k in NOISE_KEYS
170
-
171
-
172
  SHORT_NAMES = {
173
  "regularMarketPrice":"Price",
174
  "regularMarketChange":"Chg",
175
  "regularMarketChangePercent":"Chg%",
176
- "regularMarketPreviousClose":"Prev",
177
  "regularMarketOpen":"Open",
178
  "regularMarketDayHigh":"High",
179
  "regularMarketDayLow":"Low",
180
  "regularMarketVolume":"Vol",
181
  "marketCap":"MCap",
182
- "beta":"Beta",
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:
@@ -204,47 +191,70 @@ def classify_key(k, v):
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
 
@@ -255,8 +265,8 @@ def build_df_from_dict(data):
255
  def fetch_info(symbol):
256
  key = f"info_{symbol}"
257
 
258
- if exists(key, "html"):
259
- cached = load(key, "html")
260
  if cached:
261
  return cached
262
 
@@ -265,54 +275,70 @@ def fetch_info(symbol):
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")
315
-
316
  return html
317
 
318
  except Exception:
 
10
 
11
 
12
  # ==============================
13
+ # Yahoo Finance fetch
14
  # ==============================
15
  def yfinfo(symbol):
16
  try:
 
26
  # ==============================
27
  MAIN_ICONS = {
28
  "Price / Volume": "πŸ“ˆ",
29
+ "Fundamentals": "πŸ“Š",
30
+ "Valuation": "πŸ’°",
31
+ "Trend": "πŸ“ˆ",
32
+ "Signals": "🧠",
33
  "Company Profile": "🏒",
34
  "Management": "πŸ‘”"
35
  }
36
 
37
 
38
  # ==============================
39
+ # Layout helpers
40
  # ==============================
41
  def column_layout(html):
42
  return f"""
 
52
  @media(max-width:640px) {{
53
  .grid {{ grid-template-columns:1fr; }}
54
  }}
55
+ .pos {{ color:#0a7d32;font-weight:600; }}
56
+ .neg {{ color:#b00020;font-weight:600; }}
57
  </style>
58
  <div class="grid">{html}</div>
59
  """
 
73
  def html_card(title, body, mini=False, shade=0):
74
  font = "12px" if mini else "14px"
75
  pad = "6px" if mini else "10px"
76
+ shades = ["#e6f0fa","#d7e3f5","#c8d6f0"]
 
 
 
 
 
 
77
 
78
  return f"""
79
  <div style="background:{shades[shade%3]};
 
81
  border-radius:8px;
82
  padding:{pad};
83
  font-size:{font};">
84
+ <div style="font-weight:600;margin-bottom:6px;">
 
 
 
 
 
85
  {title}
86
  </div>
87
  {body}
 
90
 
91
 
92
  # ==============================
93
+ # Formatting
94
  # ==============================
95
  def human_number(n):
96
  try:
 
105
 
106
  def format_date(v):
107
  try:
108
+ return datetime.fromtimestamp(v, tz=timezone.utc).strftime("%d %b %Y")
 
 
109
  except:
110
  return v
111
 
112
 
113
  def format_value(k, v):
114
  lk = k.lower()
 
115
  cls = ""
 
116
  if isinstance(v, (int, float)):
117
+ cls = "pos" if v > 0 else "neg" if v < 0 else ""
 
 
118
  if "percent" in lk:
119
+ return f'<span class="{cls}">{v:.2f}%</span>'
120
+ if any(x in lk for x in ["marketcap","revenue","income","value"]):
121
  return f'<span class="{cls}">β‚Ή{human_number(v)}</span>'
122
  return f'<span class="{cls}">{human_number(v)}</span>'
123
 
 
127
  return v
128
 
129
 
 
 
 
130
  def make_table(df):
131
  return "".join(
132
  f"""
133
  <div style="display:flex;justify-content:space-between;
134
  border-bottom:1px dashed #bcd0ea;padding:2px 0;">
135
+ <span>{r.Field}</span>
136
  <span>{r.Value}</span>
137
  </div>
138
  """
 
141
 
142
 
143
  # ==============================
144
+ # Keys
145
  # ==============================
146
  NOISE_KEYS = {
147
  "maxAge","priceHint","triggerable",
 
150
  "esgPopulated"
151
  }
152
 
 
 
 
153
  SHORT_NAMES = {
154
  "regularMarketPrice":"Price",
155
  "regularMarketChange":"Chg",
156
  "regularMarketChangePercent":"Chg%",
 
157
  "regularMarketOpen":"Open",
158
  "regularMarketDayHigh":"High",
159
  "regularMarketDayLow":"Low",
160
  "regularMarketVolume":"Vol",
161
  "marketCap":"MCap",
162
+ "trailingPE":"PE",
163
+ "forwardPE":"FwdPE",
164
+ "priceToBook":"PB",
165
+ "epsTrailingTwelveMonths":"EPS",
166
+ "returnOnEquity":"ROE",
167
+ "returnOnAssets":"ROA",
168
+ "profitMargins":"Margin",
169
+ "debtToEquity":"D/E"
170
  }
171
 
172
+ PIN_PRICE = ["Price","Chg","Chg%","Open","High","Low","Vol"]
173
+ PIN_FUND = ["MCap","PE","PB","EPS","ROE","ROA","Margin","D/E"]
 
 
174
 
175
+ def pretty_key(k):
176
+ return SHORT_NAMES.get(k, k[:14])
177
 
178
 
179
  # ==============================
180
+ # Classification
181
  # ==============================
182
+ def classify(k, v):
183
+ lk = k.lower()
184
+ if k == "companyOfficers": return "management"
185
+ if any(x in lk for x in ["pe","pb","roe","roa","margin","debt","revenue","profit","eps","cap"]):
186
+ return "fundamental"
187
  if isinstance(v, (int, float)):
188
  return "price_volume"
189
  if isinstance(v, str) and len(v) > 80:
 
191
  return "profile"
192
 
193
 
194
+ def group_info(info):
195
+ g = {"price_volume":{}, "fundamental":{}, "profile":{},
196
+ "management":{}, "long_text":{}}
197
+ for k,v in info.items():
198
+ if k in NOISE_KEYS or v in [None,"",[],{}]:
 
 
 
 
199
  continue
200
+ g[classify(k,v)][k] = v
201
  return g
202
 
203
 
204
  # ==============================
205
+ # Builders
206
  # ==============================
207
+ def build_df(data, pinned=None):
208
+ rows = []
209
+ for k,v in data.items():
210
+ rows.append((pretty_key(k), format_value(k,v)))
211
+ pinned = pinned or []
212
+ rows.sort(key=lambda x: (0 if x[0] in pinned else 1,
213
+ pinned.index(x[0]) if x[0] in pinned else x[0]))
214
+ return pd.DataFrame(rows, columns=["Field","Value"])
215
+
216
+
217
+ def split_df(df):
218
  n = len(df)
219
  cols = 1 if n <= 6 else 2 if n <= 14 else 3
220
+ size = (n + cols - 1) // cols
221
+ return [df.iloc[i:i+size] for i in range(0, n, size)]
222
 
223
 
224
  # ==============================
225
+ # Trend & Signals
226
  # ==============================
227
+ def build_trend(info):
228
  rows = []
229
+ p = info.get("regularMarketPrice")
230
+ l = info.get("fiftyTwoWeekLow")
231
+ h = info.get("fiftyTwoWeekHigh")
232
+ d50 = info.get("fiftyDayAverage")
233
+ beta = info.get("beta")
234
+
235
+ if p and l and h:
236
+ rows.append(("52W Position", f"{(p-l)/(h-l)*100:.1f}%"))
237
+ if p and d50:
238
+ rows.append(("vs 50DMA", "Above ↑" if p>d50 else "Below ↓"))
239
+ if beta:
240
+ rows.append(("Risk", "High" if beta>1.3 else "Low" if beta<0.8 else "Moderate"))
241
+
242
+ return pd.DataFrame(rows, columns=["Field","Value"])
243
+
244
+
245
+ def build_signals(info):
246
+ rows = []
247
+ pe = info.get("trailingPE")
248
+ roe = info.get("returnOnEquity")
249
+ p = info.get("regularMarketPrice")
250
+ d50 = info.get("fiftyDayAverage")
251
+
252
+ if pe:
253
+ rows.append(("Valuation","Expensive" if pe>35 else "Cheap" if pe<15 else "Fair"))
254
+ if p and d50:
255
+ rows.append(("Momentum","Strong" if p>d50 else "Weak"))
256
+ if roe:
257
+ rows.append(("Quality","High" if roe>0.18 else "Average"))
258
 
259
  return pd.DataFrame(rows, columns=["Field","Value"])
260
 
 
265
  def fetch_info(symbol):
266
  key = f"info_{symbol}"
267
 
268
+ if exists(key,"html"):
269
+ cached = load(key,"html")
270
  if cached:
271
  return cached
272
 
 
275
  if "__error__" in info:
276
  return "No data"
277
 
278
+ g = group_info(info)
279
  html = ""
280
 
 
281
  if g["price_volume"]:
282
+ df = build_df(g["price_volume"], PIN_PRICE)
 
 
 
 
283
  html += collapsible(
284
  f"{MAIN_ICONS['Price / Volume']} Price / Volume",
285
+ column_layout("".join(
286
+ html_card("Price", make_table(c), mini=True)
287
+ for c in split_df(df)
288
+ ))
289
  )
290
 
291
+ if g["fundamental"]:
292
+ df = build_df(g["fundamental"], PIN_FUND)
293
+ html += collapsible(
294
+ f"{MAIN_ICONS['Fundamentals']} Fundamentals",
295
+ column_layout("".join(
296
+ html_card("Fundamentals", make_table(c), mini=True)
297
+ for c in split_df(df)
298
+ ))
299
  )
300
+
301
+ trend = build_trend(info)
302
+ if not trend.empty:
303
+ html += collapsible(
304
+ f"{MAIN_ICONS['Trend']} Trend Summary",
305
+ html_card("Trend", make_table(trend), mini=True)
306
+ )
307
+
308
+ sig = build_signals(info)
309
+ if not sig.empty:
310
+ html += collapsible(
311
+ f"{MAIN_ICONS['Signals']} Smart Signals",
312
+ html_card("Signals", make_table(sig), mini=True)
313
+ )
314
+
315
+ if g["profile"]:
316
+ df = build_df(g["profile"])
317
  html += collapsible(
318
  f"{MAIN_ICONS['Company Profile']} Company Profile",
319
+ column_layout("".join(
320
+ html_card("Profile", make_table(c), mini=True)
321
+ for c in split_df(df)
322
+ ))
323
  )
324
 
 
325
  if g["management"].get("companyOfficers"):
326
+ cards = ""
327
  for o in g["management"]["companyOfficers"]:
328
+ cards += html_card(
329
  o.get("name",""),
330
+ o.get("title",""),
331
  mini=True
332
  )
333
  html += collapsible(
334
  f"{MAIN_ICONS['Management']} Management",
335
+ column_layout(cards)
336
  )
337
 
 
338
  for k,v in g["long_text"].items():
339
  html += collapsible(pretty_key(k), v)
340
 
341
+ save(key, html, "html")
 
 
342
  return html
343
 
344
  except Exception: