eshan6704 commited on
Commit
a273191
·
verified ·
1 Parent(s): 00a4f7a

Update app/yahooinfo.py

Browse files
Files changed (1) hide show
  1. app/yahooinfo.py +317 -1
app/yahooinfo.py CHANGED
@@ -1,4 +1,4 @@
1
- # ==============================
2
  # Imports
3
  # ==============================
4
  import yfinance as yf
@@ -315,3 +315,319 @@ def fetch_info(symbol):
315
  return html
316
  except Exception:
317
  return f"<pre>{traceback.format_exc()}</pre>"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ '''# ==============================
2
  # Imports
3
  # ==============================
4
  import yfinance as yf
 
315
  return html
316
  except Exception:
317
  return f"<pre>{traceback.format_exc()}</pre>"
318
+ '''
319
+ # ==============================
320
+ # Imports
321
+ # ==============================
322
+ import yfinance as yf
323
+ import pandas as pd
324
+ import traceback
325
+ from datetime import datetime, timezone
326
+
327
+ from .persist import exists, load, save
328
+
329
+ # ==============================
330
+ # Icons
331
+ # ==============================
332
+ MAIN_ICONS = {
333
+ "Price / Volume": "📈",
334
+ "Fundamentals": "📊",
335
+ "Trend": "📈",
336
+ "Signals": "🧠",
337
+ "Company Profile": "🏢",
338
+ "Management": "👔",
339
+ "VWAP": "📌",
340
+ "IPO": "🚀"
341
+ }
342
+
343
+ # ==============================
344
+ # Short names
345
+ # ==============================
346
+ SHORT_NAMES = {
347
+ "regularMarketPrice": "Price",
348
+ "regularMarketChange": "Chg",
349
+ "regularMarketChangePercent": "Chg%",
350
+ "regularMarketPreviousClose": "Prev",
351
+ "regularMarketOpen": "Open",
352
+ "regularMarketDayHigh": "High",
353
+ "regularMarketDayLow": "Low",
354
+ "regularMarketVolume": "Vol",
355
+ "averageDailyVolume10Day": "Avg Vol 10D",
356
+ "averageDailyVolume3Month": "Avg Vol 3M",
357
+ "fiftyDayAverage": "50DMA",
358
+ "twoHundredDayAverage": "200DMA",
359
+ "fiftyTwoWeekLow": "52W Low",
360
+ "fiftyTwoWeekHigh": "52W High",
361
+ "mostRecentQuarter":"Recent Q",
362
+ "lastFiscalYearEnd":"FY End",
363
+ "vwap":"VWAP",
364
+ "dailyGapPercent":"Gap%",
365
+ "dailyRangePercent":"Range%",
366
+ "firstTradeDate":"IPO Date"
367
+ }
368
+
369
+ # ==============================
370
+ # Price / Volume Sub-Groups
371
+ # ==============================
372
+ PRICE_VOLUME_GROUPS = {
373
+ "Market Price": ["Price","Chg","Chg%","Prev","Open","firstTradeDate"],
374
+ "Intraday Range": ["High","Low","dailyRangePercent"],
375
+ "Volume & Liquidity": ["Vol","Avg Vol 10D","Avg Vol 3M"],
376
+ "Moving Averages": ["50DMA","200DMA","vs 50DMA","vs 200DMA"],
377
+ "52W Range": ["52W Low","52W High","52W Pos"],
378
+ "VWAP & Gap": ["VWAP","dailyGapPercent"]
379
+ }
380
+
381
+ # ==============================
382
+ # Noise keys
383
+ # ==============================
384
+ NOISE_KEYS = {
385
+ "maxAge","priceHint","triggerable",
386
+ "customPriceAlertConfidence",
387
+ "sourceInterval","exchangeDataDelayedBy",
388
+ "esgPopulated"
389
+ }
390
+
391
+ # ==============================
392
+ # Low-level Yahoo fetch
393
+ # ==============================
394
+ def yfinfo(symbol):
395
+ try:
396
+ t = yf.Ticker(symbol + ".NS")
397
+ info = t.info
398
+ return info if isinstance(info, dict) else {}
399
+ except Exception as e:
400
+ return {"__error__": str(e)}
401
+
402
+ # ==============================
403
+ # Formatting
404
+ # ==============================
405
+ def human_number(n):
406
+ try:
407
+ n = float(n)
408
+ if abs(n) >= 1e7: return f"{n/1e7:.2f}Cr"
409
+ if abs(n) >= 1e5: return f"{n/1e5:.2f}L"
410
+ if abs(n) >= 1e3: return f"{n/1e3:.2f}K"
411
+ return f"{n:,.2f}"
412
+ except:
413
+ return str(n)
414
+
415
+ DATE_KEYWORDS = ("date", "time", "timestamp", "fiscal", "quarter","earnings","dividend","firstTradeDate")
416
+ def looks_like_unix_ts(v):
417
+ try:
418
+ v = int(v)
419
+ return (946684800 <= v <= 4102444800 or 946684800000 <= v <= 4102444800000)
420
+ except:
421
+ return False
422
+ def unix_to_dt(v):
423
+ v = int(v)
424
+ if v > 10**12: v //= 1000
425
+ return datetime.fromtimestamp(v, tz=timezone.utc)
426
+ def fy_quarter_label(dt):
427
+ y, m = dt.year, dt.month
428
+ if m >= 4:
429
+ fy = y + 1
430
+ q = (m - 1)//3 + 1
431
+ else:
432
+ fy = y
433
+ q = (m + 8)//3
434
+ return f"Q{q} FY{str(fy)[-2:]}"
435
+ def format_value(k, v):
436
+ lk = k.lower()
437
+ # Date / Quarter
438
+ if isinstance(v,(int,float)) and looks_like_unix_ts(v):
439
+ if any(x in lk for x in DATE_KEYWORDS):
440
+ dt = unix_to_dt(v)
441
+ if "quarter" in lk:
442
+ return fy_quarter_label(dt)
443
+ return dt.strftime("%d %b %Y")
444
+ # Numbers
445
+ if isinstance(v,(int,float)):
446
+ cls = "pos" if v>0 else "neg" if v<0 else ""
447
+ if "percent" in lk:
448
+ return f'<span class="{cls}">{v:.2f}%</span>'
449
+ if any(x in lk for x in ["marketcap","revenue","income","value","cap","enterprise"]):
450
+ return f'<span class="{cls}">₹{human_number(v)}</span>'
451
+ return f'<span class="{cls}">{human_number(v)}</span>'
452
+ # Strings
453
+ if isinstance(v,str) and len(v)>80:
454
+ return f'<div style="font-size:12px;">{v}</div>'
455
+ return v
456
+
457
+ # ==============================
458
+ # HTML Helpers
459
+ # ==============================
460
+ def column_layout(html):
461
+ return f"""
462
+ <style>
463
+ .grid{{display:grid;gap:10px;grid-template-columns:repeat(3,1fr);}}
464
+ @media(max-width:1024px){{.grid{{grid-template-columns:repeat(2,1fr);}}}}
465
+ @media(max-width:640px){{.grid{{grid-template-columns:1fr;}}}}
466
+ .pos{{color:#0a7d32;font-weight:600;}}
467
+ .neg{{color:#b00020;font-weight:600;}}
468
+ .alert{{color:#d97706;font-weight:600;}}
469
+ </style>
470
+ <div class="grid">{html}</div>
471
+ """
472
+ def html_card(title,body,mini=False):
473
+ font = "12px" if mini else "14px"
474
+ pad = "6px" if mini else "10px"
475
+ return f"""
476
+ <div style="background:#e6f0fa;border:1px solid #a3c0e0;border-radius:8px;padding:{pad};
477
+ font-size:{font};margin-bottom:6px;">
478
+ <div style="font-weight:600;margin-bottom:6px;">{title}</div>{body}
479
+ </div>
480
+ """
481
+ def make_table(df):
482
+ return "".join(
483
+ f"""<div style="display:flex;justify-content:space-between;border-bottom:1px dashed #bcd0ea;padding:2px 0;">
484
+ <span>{r.Field}</span><span>{r.Value}</span></div>"""
485
+ for r in df.itertuples()
486
+ )
487
+
488
+ # ==============================
489
+ # Data Helpers
490
+ # ==============================
491
+ def build_df_from_dict(data):
492
+ rows = [(SHORT_NAMES.get(k,k[:16]), format_value(k,v)) for k,v in data.items() if k not in NOISE_KEYS]
493
+ return pd.DataFrame(rows,columns=["Field","Value"])
494
+
495
+ def resolve_duplicates(data):
496
+ DUP = {
497
+ "price":["regularMarketPrice","currentPrice"],
498
+ "prev":["regularMarketPreviousClose","previousClose"],
499
+ "open":["regularMarketOpen","open"],
500
+ "high":["regularMarketDayHigh","dayHigh"],
501
+ "low":["regularMarketDayLow","dayLow"],
502
+ "volume":["regularMarketVolume","volume"]
503
+ }
504
+ resolved, used = {}, set()
505
+ for keys in DUP.values():
506
+ for k in keys:
507
+ if k in data:
508
+ resolved[k] = data[k]
509
+ used.update(keys)
510
+ break
511
+ for k,v in data.items():
512
+ if k not in used:
513
+ resolved[k] = v
514
+ return resolved
515
+
516
+ def classify(k,v):
517
+ lk = k.lower()
518
+ if k=="companyOfficers": return "management"
519
+ if any(x in lk for x in ["pe","pb","roe","roa","margin","debt","revenue","profit","eps","cap"]):
520
+ return "fundamental"
521
+ if isinstance(v,(int,float)): return "price_volume"
522
+ if isinstance(v,str) and len(v)>80: return "long_text"
523
+ return "profile"
524
+
525
+ def group_info(info):
526
+ g = {"price_volume":{}, "fundamental":{}, "profile":{}, "management":{}, "long_text":{}}
527
+ for k,v in info.items():
528
+ if k in NOISE_KEYS or v in [None,"",[],{}]: continue
529
+ g[classify(k,v)][k] = v
530
+ return g
531
+
532
+ def split_df(df):
533
+ n = len(df)
534
+ cols = 1 if n<=6 else 2 if n<=14 else 3
535
+ size = (n+cols-1)//cols
536
+ return [df.iloc[i:i+size] for i in range(0,n,size)]
537
+
538
+ # ==============================
539
+ # Derived Metrics & Signals
540
+ # ==============================
541
+ def build_price_volume_derived(info):
542
+ out={}
543
+ price=info.get("regularMarketPrice")
544
+ dma50=info.get("fiftyDayAverage")
545
+ dma200=info.get("twoHundredDayAverage")
546
+ low52=info.get("fiftyTwoWeekLow")
547
+ high52=info.get("fiftyTwoWeekHigh")
548
+ if price and dma50: out["vs 50DMA"]="Above ↑" if price>dma50 else "Below ↓"
549
+ if price and dma200: out["vs 200DMA"]="Above ↑" if price>dma200 else "Below ↓"
550
+ if price and low52 and high52 and high52!=low52: out["52W Pos"]=f"{(price-low52)/(high52-low52)*100:.1f}%"
551
+ prev=info.get("regularMarketPreviousClose")
552
+ high=info.get("regularMarketDayHigh")
553
+ low=info.get("regularMarketDayLow")
554
+ if price and prev: out["dailyGapPercent"]=(price-prev)/prev*100
555
+ if high and low and price: out["dailyRangePercent"]=(high-low)/price*100
556
+ vol=info.get("regularMarketVolume")
557
+ if vol and price: out["VWAP"]=price
558
+ if info.get("firstTradeDate"): out["firstTradeDate"]=info.get("firstTradeDate")
559
+ return out
560
+
561
+ def build_smart_signals(info):
562
+ rows=[]
563
+ pe=info.get("trailingPE")
564
+ roe=info.get("returnOnEquity")
565
+ debt=info.get("debtToEquity")
566
+ price=info.get("regularMarketPrice")
567
+ dma50=info.get("fiftyDayAverage")
568
+ dma200=info.get("twoHundredDayAverage")
569
+ # Alerts
570
+ if pe: rows.append(("Valuation",f'<span class="alert">{"Expensive" if pe>35 else "Cheap" if pe<15 else "Fair"}</span>'))
571
+ if roe: rows.append(("Quality",f'<span class="alert">{"High" if roe>0.18 else "Average"}</span>'))
572
+ if debt: rows.append(("Balance Sheet",f'<span class="alert">{"Weak" if debt>1 else "Healthy"}</span>'))
573
+ if price and dma50 and dma200:
574
+ trend = "Bullish" if price>dma50>dma200 else "Bearish" if price<dma50<dma200 else "Neutral"
575
+ rows.append(("Momentum",f'<span class="alert">{trend}</span>'))
576
+ return pd.DataFrame(rows,columns=["Field","Value"])
577
+
578
+ # ==============================
579
+ # Build Price/Volume Section
580
+ # ==============================
581
+ def build_price_volume_section(info,pv_data):
582
+ df=build_df_from_dict(pv_data)
583
+ derived=build_price_volume_derived(info)
584
+ if derived: df=pd.concat([df,pd.DataFrame(derived.items(),columns=["Field","Value"])],ignore_index=True)
585
+ cards=""
586
+ for title,fields in PRICE_VOLUME_GROUPS.items():
587
+ sub=df[df["Field"].isin(fields)]
588
+ if not sub.empty: cards+=html_card(title,make_table(sub),mini=True)
589
+ trend_df=df[df["Field"].isin(["vs 50DMA","vs 200DMA","52W Pos"])]
590
+ if not trend_df.empty: cards+=html_card("Trend & Momentum",make_table(trend_df),mini=True)
591
+ signals=build_smart_signals(info)
592
+ if not signals.empty: cards+=html_card("Smart Signals",make_table(signals),mini=True)
593
+ return column_layout(cards)
594
+
595
+ # ==============================
596
+ # Main Function
597
+ # ==============================
598
+ def fetch_info(symbol):
599
+ key=f"info_{symbol}"
600
+ if exists(key,"html"):
601
+ cached=load(key,"html")
602
+ if cached: return cached
603
+ try:
604
+ info=yfinfo(symbol)
605
+ if "__error__" in info: return "No data"
606
+ groups=group_info(info)
607
+ html=""
608
+ # Price / Volume
609
+ pv=resolve_duplicates(groups["price_volume"])
610
+ if pv: html+=html_card(f"{MAIN_ICONS['Price / Volume']} Price / Volume",build_price_volume_section(info,pv))
611
+ # Fundamentals
612
+ if groups["fundamental"]:
613
+ df=build_df_from_dict(groups["fundamental"])
614
+ html+=html_card(f"{MAIN_ICONS['Fundamentals']} Fundamentals",
615
+ column_layout("".join(html_card("Fundamentals",make_table(c),mini=True) for c in split_df(df))))
616
+ # Company Profile
617
+ if groups["profile"]:
618
+ df=build_df_from_dict(groups["profile"])
619
+ html+=html_card(f"{MAIN_ICONS['Company Profile']} Company Profile",
620
+ column_layout("".join(html_card("Profile",make_table(c),mini=True) for c in split_df(df))))
621
+ # Management
622
+ if groups["management"].get("companyOfficers"):
623
+ cards=""
624
+ for o in groups["management"]["companyOfficers"]:
625
+ cards+=html_card(o.get("name",""),o.get("title",""),mini=True)
626
+ html+=html_card(f"{MAIN_ICONS['Management']} Management",column_layout(cards))
627
+ # Long Text
628
+ for k,v in groups["long_text"].items():
629
+ html+=html_card(SHORT_NAMES.get(k,k[:16]),v)
630
+ save(key,html,"html")
631
+ return html
632
+ except Exception:
633
+ return f"<pre>{traceback.format_exc()}</pre>"