# ============================== # Imports # ============================== import yfinance as yf import pandas as pd import traceback import time from datetime import datetime, timezone # persist helpers from .persist import exists, load, save # ============================== # Yahoo Finance info fetch (RAW) # ============================== def yfinfo(symbol): """ Low-level Yahoo Finance info fetch. Returns raw dict or {"__error__": "..."} """ try: t = yf.Ticker(symbol + ".NS") info = t.info if not info or not isinstance(info, dict): return {} return info except Exception as e: return {"__error__": str(e)} # ============================== # Icons # ============================== SUBGROUP_ICONS = { "Live Price": "💹", "Volume": "📊", "Moving Avg": "📈", "Range / Vol": "📉", "Bid / Analyst": "📝", "Other": "â„šī¸" } MAIN_ICONS = { "Price / Volume": "📈", "Fundamentals": "📊", "Company Profile": "đŸĸ" } # ============================== # Responsive column layout # ============================== def column_layout(html, min_width=320): return f"""
{html}
""" # ============================== # Card renderer # ============================== def html_card(title, body, mini=False, shade=0): font = "12px" if mini else "14px" pad = "6px" if mini else "10px" shades = ["#e6f0fa", "#d7e3f5", "#c8d6f0"] grads = [ "linear-gradient(to right,#1a4f8a,#4a7ac7)", "linear-gradient(to right,#1f5595,#5584d6)", "linear-gradient(to right,#205ca0,#6192e0)" ] return f"""
{title}
{body}
""" # ============================== # Formatting helpers # ============================== def format_number(x): try: x = float(x) if abs(x) >= 100: return f"{x:,.0f}" if abs(x) >= 1: return f"{x:,.2f}" return f"{x:.4f}" except: return str(x) # ============================== # Compact inline key:value view # ============================== def make_table(df): rows = "" for _, r in df.iterrows(): color = "#0d1f3c" if any(x in r[0].lower() for x in ["chg", "%"]): try: color = "#0a7d32" if float(r[1]) >= 0 else "#b00020" except: pass rows += f"""
{r[0]} {r[1]}
""" return f"
{rows}
" # ============================== # Noise filtering # ============================== NOISE_KEYS = { "maxAge","priceHint","triggerable", "customPriceAlertConfidence", "sourceInterval","exchangeDataDelayedBy", "esgPopulated" } def is_noise(k): return k in NOISE_KEYS # ============================== # Duplicate resolver # ============================== DUPLICATE_PRIORITY = { "price": ["regularMarketPrice","currentPrice"], "prev": ["regularMarketPreviousClose","previousClose"], "open": ["regularMarketOpen","open"], "high": ["regularMarketDayHigh","dayHigh"], "low": ["regularMarketDayLow","dayLow"], "volume": ["regularMarketVolume","volume"] } def resolve_duplicates(data): resolved, used = {}, set() for keys in DUPLICATE_PRIORITY.values(): for k in keys: if k in data: resolved[k] = data[k] used.update(keys) break for k,v in data.items(): if k not in used: resolved[k] = v return resolved # ============================== # Short key names # ============================== SHORT_NAMES = { "regularMarketPrice":"Price", "regularMarketChange":"Chg", "regularMarketChangePercent":"Chg%", "regularMarketPreviousClose":"Prev", "regularMarketOpen":"Open", "regularMarketDayHigh":"High", "regularMarketDayLow":"Low", "regularMarketVolume":"Vol", "averageDailyVolume10Day":"AvgV10", "averageDailyVolume3Month":"AvgV3M", "fiftyDayAverage":"50DMA", "twoHundredDayAverage":"200DMA", "fiftyTwoWeekLow":"52WL", "fiftyTwoWeekHigh":"52WH", "beta":"Beta", "targetMeanPrice":"Target" } def pretty_key(k): return SHORT_NAMES.get(k, k[:12]) # ============================== # Classifiers # ============================== def classify_price_volume_subgroup(key): k = key.lower() if "volume" in k: return "Volume" if "average" in k or "dma" in k: return "Moving Avg" if "week" in k or "beta" in k: return "Range / Vol" if "target" in k or "recommend" in k: return "Bid / Analyst" return "Live Price" def classify_key(key, value): k = key.lower() if isinstance(value,(int,float)) and any(x in k for x in [ "price","volume","avg","change","percent","market","week","beta","target" ]): return "price_volume" if any(x in k for x in [ "revenue","income","profit","margin","pe","pb","roe","roa","debt","equity" ]): return "fundamental" if isinstance(value,str) and len(value) > 80: return "long_text" return "profile" # ============================== # Group builder # ============================== def build_grouped_info(info): groups = { "price_volume":{}, "fundamental":{}, "profile":{}, "long_text":{} } for k,v in info.items(): if v in [None,"",[],{}]: continue groups[classify_key(k,v)][k] = v return groups # ============================== # Column splitter # ============================== def split_df_evenly(df): if df is None or df.empty: return [] n = len(df) cols = 1 if n <= 6 else 2 if n <= 14 else 3 chunk = (n + cols - 1) // cols return [df.iloc[i:i+chunk] for i in range(0, n, chunk)] # ============================== # DataFrame builder # ============================== def build_df_from_dict(data): rows = [] for k,v in data.items(): if is_noise(k): continue if isinstance(v,(int,float)): v = format_number(v) rows.append([pretty_key(k), v]) return pd.DataFrame(rows, columns=["Field","Value"]) # ============================== # MAIN FUNCTION (CACHED) # ============================== def fetch_info(symbol): """ Cached Yahoo Finance info renderer Cache validity: 1 hour """ key = f"info_{symbol}" if exists(key, "html"): cached = load(key, "html") if cached: return cached try: info = yfinfo(symbol) if not info or "__error__" in info: return "No data" groups = build_grouped_info(info) html = "" # PRICE / VOLUME pv = resolve_duplicates(groups["price_volume"]) sub = {} for k,v in pv.items(): sg = classify_price_volume_subgroup(k) sub.setdefault(sg,{})[k] = v cards = "" for i,(t,d) in enumerate(sub.items()): df = build_df_from_dict(d) if not df.empty: cards += html_card( f"{SUBGROUP_ICONS.get(t,'â„šī¸')} {t}", make_table(df), mini=True, shade=i ) if cards: html += html_card( f"{MAIN_ICONS['Price / Volume']} Price / Volume", column_layout(cards), shade=0 ) # FUNDAMENTALS if groups["fundamental"]: chunks = split_df_evenly(build_df_from_dict(groups["fundamental"])) cols = "".join( html_card("📊 Fundamentals", make_table(c), mini=True, shade=i) for i,c in enumerate(chunks) ) html += html_card( f"{MAIN_ICONS['Fundamentals']} Fundamentals", column_layout(cols), shade=1 ) # PROFILE if groups["profile"]: chunks = split_df_evenly(build_df_from_dict(groups["profile"])) cols = "".join( html_card("đŸĸ Profile", make_table(c), mini=True, shade=i) for i,c in enumerate(chunks) ) html += html_card( f"{MAIN_ICONS['Company Profile']} Company Profile", column_layout(cols), shade=2 ) # LONG TEXT for k,v in groups["long_text"].items(): html += html_card(pretty_key(k), v, shade=2) if html.strip(): save(key, html, "html") return html except Exception: return f"
{traceback.format_exc()}
"