CoolFace
Apppublic

vchaang/IPO-tracker

sourceHugging Faceupdated 7mo agoView on Hugging Face
0likes
streamlit_app.py431 linesDownload Raw Back to src
1import streamlit as st2import yfinance as yf3import pandas as pd4import time5import requests6import random7import urllib.parse8from datetime import timedelta, datetime9 10# --- PAGE CONFIG ---11st.set_page_config(page_title="Catalyst & Flow Tracker", layout="wide")12 13# --- CUSTOM CSS FOR STYLING ---14st.markdown("""15<style>16    /* Modern, elegant, minimalist styling */17    .metric-card {18        background: rgba(128, 128, 128, 0.05);19        backdrop-filter: blur(10px);20        padding: 24px 16px;21        border-radius: 8px;22        border: 1px solid rgba(128, 128, 128, 0.2);23        text-align: center;24        transition: all 0.3s ease;25    }26    .metric-card:hover {27        border-color: rgba(128, 128, 128, 0.4);28    }29    .metric-label { 30        font-size: 11px; 31        text-transform: uppercase; 32        letter-spacing: 1.5px; 33        color: #888888; 34        margin-bottom: 8px; 35        font-weight: 600;36    }37    .metric-value { 38        font-size: 28px; 39        font-weight: 300; 40        letter-spacing: -0.5px; 41    }42    .pos-return { color: #5C946E !important; }43    .neg-return { color: #C96464 !important; }44    h1, h2, h3 { font-weight: 400 !important; letter-spacing: -0.5px; }45</style>46""", unsafe_allow_html=True)47 48# --- CACHED DATA FETCHING ---49@st.cache_data(ttl=3600, show_spinner=False)50def fetch_stock_data(ticker):51    """52    Highly resilient fetcher designed for blocked server environments.53    Tries yfinance first, then raw HTTP, then routes through public proxies.54    """55    stock = yf.Ticker(ticker)56    hist_max = pd.DataFrame()57    58    user_agents = [59        'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36',60        'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'61    ]62 63    # Strategy 1: Standard yfinance history call64    try:65        hist_max = stock.history(period="max")66    except Exception:67        pass 68        69    # Strategy 2: RAW HTTP Fallback (Direct)70    if hist_max is None or hist_max.empty:71        endpoints = [72            f"https://query1.finance.yahoo.com/v8/finance/chart/{ticker}?range=max&interval=1d",73            f"https://query2.finance.yahoo.com/v8/finance/chart/{ticker}?range=max&interval=1d"74        ]75        for url in endpoints:76            try:77                headers = {'User-Agent': random.choice(user_agents)}78                res = requests.get(url, headers=headers, timeout=5)79                if res.status_code == 200:80                    data = res.json()81                    chart_res = data.get('chart', {}).get('result', [{}])[0]82                    timestamps = chart_res.get('timestamp', [])83                    closes = chart_res.get('indicators', {}).get('quote', [{}])[0].get('close', [])84                    if timestamps and closes:85                        hist_max = pd.DataFrame({'Close': closes}, index=pd.to_datetime(timestamps, unit='s', utc=True))86                        hist_max = hist_max.dropna()87                        break88            except Exception:89                continue90 91    # Strategy 3: PROXY ROUTER (Bypasses Hugging Face IP Blocks Entirely)92    if hist_max is None or hist_max.empty:93        target_url = f"https://query2.finance.yahoo.com/v8/finance/chart/{ticker}?range=max&interval=1d"94        encoded_url = urllib.parse.quote(target_url, safe='')95        proxies = [96            f"https://api.allorigins.win/raw?url={encoded_url}",97            f"https://api.codetabs.com/v1/proxy?quest={encoded_url}"98        ]99        for proxy_url in proxies:100            try:101                res = requests.get(proxy_url, timeout=10)102                if res.status_code == 200:103                    data = res.json()104                    chart_res = data.get('chart', {}).get('result', [{}])[0]105                    timestamps = chart_res.get('timestamp', [])106                    closes = chart_res.get('indicators', {}).get('quote', [{}])[0].get('close', [])107                    if timestamps and closes:108                        hist_max = pd.DataFrame({'Close': closes}, index=pd.to_datetime(timestamps, unit='s', utc=True))109                        hist_max = hist_max.dropna()110                        break111            except Exception:112                continue113 114    # Final Check for Failure115    if hist_max is None or hist_max.empty:116        return False, f"Data completely blocked by provider firewalls. Please try a different ticker or refresh in a couple of hot minutes.", None, None, None, None117        118    ipo_date = hist_max.index.min().date()119    120    # Fetch Info (Sector, Industry, Name)121    stock_info = {'sector': 'Unknown', 'industry': 'Unknown', 'shortName': ticker}122    fast_mcap = 0123    124    # Try Strategy 1: yfinance .info125    try:126        info = stock.info127        if info is not None and 'sector' in info:128            stock_info['sector'] = info.get('sector', 'Unknown')129            stock_info['industry'] = info.get('industry', 'Unknown')130            stock_info['shortName'] = info.get('shortName', ticker)131            fast_mcap = info.get('marketCap', 0)132    except Exception:133        pass134        135    # Strategy 2 & 3: Raw HTTP & Proxy Fallback for Profile136    if stock_info['sector'] in ['Unknown', 'Unknown (Blocked)']:137        target_url = f"https://query2.finance.yahoo.com/v10/finance/quoteSummary/{ticker}?modules=summaryProfile,price"138        encoded_url = urllib.parse.quote(target_url, safe='')139        140        urls_to_try = [141            target_url, # Direct142            f"https://api.allorigins.win/raw?url={encoded_url}" # Proxy143        ]144        145        for url in urls_to_try:146            try:147                headers = {'User-Agent': random.choice(user_agents)}148                res = requests.get(url, headers=headers, timeout=5)149                if res.status_code == 200:150                    data = res.json()151                    result = data.get('quoteSummary', {}).get('result', [{}])[0]152                    profile = result.get('summaryProfile', {})153                    price_data = result.get('price', {})154                    155                    stock_info['sector'] = profile.get('sector', 'Unknown (Blocked)')156                    stock_info['industry'] = profile.get('industry', 'Unknown (Blocked)')157                    stock_info['shortName'] = price_data.get('shortName', ticker)158                    159                    if not fast_mcap:160                        fast_mcap = price_data.get('marketCap', {}).get('raw', 0)161                    break # Break if successful162            except Exception:163                stock_info['sector'] = 'Unknown (Blocked)'164                stock_info['industry'] = 'Unknown (Blocked)'165 166    # Fetch Fast Info for backup Market Cap if still 0167    if not fast_mcap:168        try:169            fast_mcap = stock.fast_info.get('marketCap', 0)170        except Exception:171            fast_mcap = 0172        173    return True, "Success", hist_max, stock_info, ipo_date, fast_mcap174 175@st.cache_data(ttl=86400, show_spinner=False)176def fetch_funds(ticker):177    """Highly resilient fetch for funds with raw HTTP and Proxy fallback."""178    try:179        df = yf.Ticker(ticker).mutualfund_holders180        if df is not None and not df.empty:181            return df182    except Exception:183        pass184        185    target_url = f"https://query2.finance.yahoo.com/v10/finance/quoteSummary/{ticker}?modules=fundOwnership"186    encoded_url = urllib.parse.quote(target_url, safe='')187    urls_to_try = [target_url, f"https://api.allorigins.win/raw?url={encoded_url}"]188    189    for url in urls_to_try:190        try:191            headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'}192            res = requests.get(url, headers=headers, timeout=5)193            if res.status_code == 200:194                data = res.json()195                owners = data.get('quoteSummary', {}).get('result', [{}])[0].get('fundOwnership', {}).get('ownershipList', [])196                if owners:197                    parsed = [{'Holder': o.get('organization', 'Unknown'), 'pctHeld': o.get('pctHeld', {}).get('raw', 0)} for o in owners]198                    return pd.DataFrame(parsed)199        except Exception:200            continue201            202    return None203 204@st.cache_data(ttl=86400, show_spinner=False)205def fetch_institutions(ticker):206    """Highly resilient fetch for institutional holders with proxy fallback."""207    try:208        df = yf.Ticker(ticker).institutional_holders209        if df is not None and not df.empty:210            return df211    except Exception:212        pass213        214    target_url = f"https://query2.finance.yahoo.com/v10/finance/quoteSummary/{ticker}?modules=institutionOwnership"215    encoded_url = urllib.parse.quote(target_url, safe='')216    urls_to_try = [target_url, f"https://api.allorigins.win/raw?url={encoded_url}"]217    218    for url in urls_to_try:219        try:220            headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'}221            res = requests.get(url, headers=headers, timeout=5)222            if res.status_code == 200:223                data = res.json()224                owners = data.get('quoteSummary', {}).get('result', [{}])[0].get('institutionOwnership', {}).get('ownershipList', [])225                if owners:226                    parsed = [{'Holder': o.get('organization', 'Unknown'), 'pctHeld': o.get('pctHeld', {}).get('raw', 0)} for o in owners]227                    return pd.DataFrame(parsed)228        except Exception:229            continue230            231    return None232 233# --- METRICS CALCULATOR ---234def calculate_metrics(hist_max):235    current_year = datetime.now().year236    if hist_max is None or hist_max.empty:237        return 0, 0, "N/A", "N/A"238        239    current_price = float(hist_max['Close'].iloc[-1])240    prev_close = float(hist_max['Close'].iloc[-2]) if len(hist_max) > 1 else current_price241    242    # YTD243    ytd_data = hist_max[hist_max.index.year == current_year]244    if not ytd_data.empty:245        first_ytd = float(ytd_data['Close'].iloc[0])246        ytd_val = ((current_price - first_ytd) / first_ytd) * 100247        ytd_return = f"{ytd_val:+.2f}%"248    else:249        ytd_return = "N/A"250        251    # 1-Year252    now_ts = pd.Timestamp.now(tz=hist_max.index.tz) if hasattr(hist_max.index, 'tz') else pd.Timestamp.now()253    one_year_ago = now_ts - pd.Timedelta(days=365)254    past_data = hist_max[hist_max.index <= one_year_ago]255    256    if not past_data.empty:257        first_1y = float(past_data['Close'].iloc[-1])258        one_yr_val = ((current_price - first_1y) / first_1y) * 100259        one_yr_return = f"{one_yr_val:+.2f}%" if len(hist_max) >= 250 else f"{one_yr_val:+.2f}% (Since IPO)"260    else:261        first_ipo = float(hist_max['Close'].iloc[0])262        one_yr_val = ((current_price - first_ipo) / first_ipo) * 100263        one_yr_return = f"{one_yr_val:+.2f}% (Since IPO)"264 265    return current_price, prev_close, ytd_return, one_yr_return266 267# --- UI LAYOUT ---268st.title("Post-IPO Catalyst & Flow Tracker")269st.markdown("<p style='color: #888; font-size: 16px; font-weight: 300;'>Predictive Index Inclusion & IPO Lock-up Mapping</p>", unsafe_allow_html=True)270st.write("")271 272# Inputs273col_search, col_override = st.columns([2, 1])274with col_search:275    ticker_input = st.text_input("Enter Ticker Only (e.g. EIKN, ARM, AAPL)", "")276with col_override:277    sector_override = st.selectbox(278        "Sector (Use if Auto-Detect fails)", 279        ["Auto-Detect", "Healthcare / Biotech", "Technology / Growth", "Other"]280    )281 282if ticker_input:283    ticker = ticker_input.upper().strip()284    with st.spinner(f"Pulling optimized market data for {ticker}..."):285        286        success, msg, hist_max, stock_info, ipo_date, fast_mcap = fetch_stock_data(ticker)287        288        if not success:289            st.error(msg)290            st.info("๐Ÿ’ก Note: Firewalls are currently extremely strict. Please try again in more than 60 seconds.")291        else:292            # Profile Data293            sector = stock_info.get('sector', 'Unknown')294            industry = stock_info.get('industry', 'Unknown')295            296            display_sector = sector297            if sector.startswith('Unknown') and sector_override != "Auto-Detect":298                display_sector = f"Manual: {sector_override}"299            300            mcap_str = f"${fast_mcap / 1e9:.2f}B" if fast_mcap else "Unknown"301            302            days_public = (datetime.now().date() - ipo_date).days303            is_mature = days_public > 365304            status_badge = "Mature Company" if is_mature else "Recent IPO"305 306            st.write("---")307            308            # Top Row: Info & Prices309            col1, col2 = st.columns([1, 2])310            with col1:311                st.subheader(f"{ticker} Profile")312                st.caption(stock_info.get('shortName', 'Company Name'))313                st.markdown(f"**Status:** {status_badge}")314                st.markdown(f"**Sector:** {display_sector}")315                st.markdown(f"**Industry:** {industry}")316                st.markdown(f"**Est. Market Cap:** {mcap_str}")317                318            with col2:319                st.subheader("Price & Performance")320                cp, pc, ytd, oyr = calculate_metrics(hist_max)321                322                m1, m2, m3, m4 = st.columns(4)323                m1.metric("Current Price", f"${cp:.2f}" if cp else "N/A", f"{cp - pc:+.2f}" if cp and pc else None)324                m2.metric("Previous Close", f"${pc:.2f}" if pc else "N/A")325                m3.metric("YTD Return", ytd)326                m4.metric("1-Year Return", oyr)327 328            st.write("---")329 330            # Middle Row: Deadlines331            st.subheader("Mechanical & Regulatory Deadlines")332            st.write("")333            334            deadlines = {335                "IPO Pricing / First Trade": ipo_date,336                "Quiet Period (T+25)*": ipo_date + timedelta(days=25),337                "Lock-Up Expiry (T+180)": ipo_date + timedelta(days=180)338            }339            340            d_cols = st.columns(3)341            for idx, (event, date) in enumerate(deadlines.items()):342                passed = date < datetime.now().date()343                status = "Passed" if passed else "Upcoming"344                color = "#888888" if passed else "#5C946E"345                346                with d_cols[idx]:347                    st.markdown(f"""348                    <div class="metric-card">349                        <div class="metric-label">{event}</div>350                        <div class="metric-value">{date.strftime('%b %d, %Y')}</div>351                        <div style="color: {color}; font-size: 11px; font-weight: 600; letter-spacing: 1px; text-transform: uppercase; margin-top: 12px;">{status}</div>352                    </div>353                    """, unsafe_allow_html=True)354 355            # Determine Sector Flags Early for conditional logic356            is_biotech = sector_override == "Healthcare / Biotech" or (sector_override == "Auto-Detect" and (sector == 'Healthcare' or 'Biotech' in industry))357            is_tech = sector_override == "Technology / Growth" or (sector_override == "Auto-Detect" and sector in ['Technology', 'Communication Services'])358 359            if is_biotech:360                st.markdown("<p style='color: #888; font-size: 13px; margin-top: 12px;'><i>*Note: Biotechs typically qualify as Emerging Growth Companies (EGCs). While legally exempt from the SEC's 25-day research quiet period, underwriting syndicates almost universally enforce the T+25 rule as strict industry practice.</i></p>", unsafe_allow_html=True)361 362            st.write("---")363 364            # Bottom Row: Index Logic365            if is_mature:366                st.subheader("Top Passive Institutional & Mutual Fund Holders")367                st.markdown(f"<p style='color: #888; font-size: 14px;'>{ticker} has been public for >1 year. Mechanical lock-ups are irrelevant. The entities listed below control the daily passive flows.</p>", unsafe_allow_html=True)368                369                col_inst, col_fund = st.columns(2)370                371                with col_inst:372                    st.markdown("**Top 5 Institutional Holders**")373                    insts = fetch_institutions(ticker)374                    375                    if insts is not None and not insts.empty:376                        insts_clean = insts.head(5)[['Holder', 'pctHeld']].copy()377                        insts_clean['pctHeld'] = (insts_clean['pctHeld'] * 100).round(2).astype(str) + '%'378                        insts_clean.columns = ['Institution Name', '% of Float Owned']379                        st.table(insts_clean)380                    else:381                        st.warning("Institutional holder data is currently unavailable (rate-limited by provider).")382 383                with col_fund:384                    st.markdown("**Top 5 Mutual Fund Holders**")385                    funds = fetch_funds(ticker)386                    387                    if funds is not None and not funds.empty:388                        funds_clean = funds.head(5)[['Holder', 'pctHeld']].copy()389                        funds_clean['pctHeld'] = (funds_clean['pctHeld'] * 100).round(2).astype(str) + '%'390                        funds_clean.columns = ['Fund Name', '% of Float Owned']391                        st.table(funds_clean)392                    else:393                        st.warning("Mutual fund holder data is currently unavailable (rate-limited by provider).")394            else:395                st.subheader("Predictive Index Inclusion Targets")396                st.write("")397                398                inclusions = []399                ipo_month = ipo_date.month400                401                if ipo_month <= 4: 402                    inclusions.append({"Index": "Russell 2000/3000", "Target": "Late June", "Prob": "High" if fast_mcap > 50_000_000 else "Low", "Rationale": "Eligible for the June Reconstitution. Usually requires >$30M market cap."})403                elif ipo_month <= 10: 404                    inclusions.append({"Index": "Russell 2000/3000", "Target": "Dec 11", "Prob": "High" if fast_mcap > 50_000_000 else "Low", "Rationale": "Eligible for the December Semi-Annual Reconstitution."})405                406                inclusions.append({"Index": "CRSP US Total Market (VTI)", "Target": "Next Quarterly Rebalance", "Prob": "High", "Rationale": "Quarterly rebalance inclusion."})407                inclusions.append({"Index": "MSCI USA IMI", "Target": "Next Index Review", "Prob": "High" if fast_mcap >= 1e9 else ("Medium" if fast_mcap >= 300_000_000 else "Low"), "Rationale": "Quarterly/Semi-Annual reviews based on liquidity/cap. High probability if >$1B."})408                inclusions.append({"Index": "S&P Composite 1500", "Target": f"After {(ipo_date + timedelta(days=365)).strftime('%b %Y')}", "Prob": "Low", "Rationale": "Requires 12 months seasoning + GAAP profitability (rare for recent IPOs)."})409                410                # Dynamic Logic based on Market Cap for Biotechs411                if is_biotech:412                    if fast_mcap > 500_000_000:413                        xbi_prob, xbi_rat = "High", "Strong market cap (>$500M). High likelihood for next quarterly rebalance (Mar/Jun/Sep/Dec)."414                        nbi_prob, nbi_rat = "High", "Strong market cap. Highly eligible for December annual reconstitution."415                    elif fast_mcap > 250_000_000:416                        xbi_prob, xbi_rat = "Medium", "Borderline market cap ($250M-$500M). Inclusion depends heavily on liquidity and trading volume."417                        nbi_prob, nbi_rat = "Medium", "May qualify for annual December reconstitution if ADV meets requirements."418                    elif fast_mcap > 0:419                        xbi_prob, xbi_rat = "Low", "Market cap below typical $300M minimum threshold for XBI."420                        nbi_prob, nbi_rat = "Low", "Market cap below typical minimums for NBI."421                    else:422                        xbi_prob, xbi_rat = "Varies", "Probability depends on final stabilized market cap (typically needs >$300M)."423                        nbi_prob, nbi_rat = "Varies", "Needs >$200M market cap by late October cut-off."424 425                    inclusions.append({"Index": "S&P Biotech (XBI)", "Target": "Next Quarterly Rebalance", "Prob": xbi_prob, "Rationale": xbi_rat})426                    inclusions.append({"Index": "Nasdaq Biotech (NBI)", "Target": "December (Annual)", "Prob": nbi_prob, "Rationale": nbi_rat})427                    inclusions.append({"Index": "ICE Biotech (IBB)", "Target": "Next Quarterly Rebalance", "Prob": xbi_prob, "Rationale": "Follows similar cap weighting and liquidity requirements to broader sector indices."})428                elif is_tech:429                    inclusions.append({"Index": "Nasdaq 100 (QQQ)", "Target": "Standard or Fast Entry (15 Days)", "Prob": "Varies", "Rationale": "Standard requires 3mo seasoning. Mega-caps fast-track in 15 Days."})430 431                st.table(pd.DataFrame(inclusions))