CoolFace
Apppublic

vchaang/IPO-tracker

sourceHugging Faceupdated 7mo agoView on Hugging Face
0likes
app.py277 linesDownload Raw Back to root
1import streamlit as st
2import yfinance as yf
3import pandas as pd
4import time
5import requests
6from datetime import timedelta, datetime
7
8# --- PAGE CONFIG ---
9st.set_page_config(page_title="Catalyst & Flow Tracker", layout="wide")
10
11# --- CUSTOM CSS FOR STYLING ---
12st.markdown("""
13<style>
14    /* Modern, elegant, minimalist styling */
15    .metric-card {
16        background: rgba(128, 128, 128, 0.05);
17        backdrop-filter: blur(10px);
18        padding: 24px 16px;
19        border-radius: 8px;
20        border: 1px solid rgba(128, 128, 128, 0.2);
21        text-align: center;
22        transition: all 0.3s ease;
23    }
24    .metric-card:hover {
25        border-color: rgba(128, 128, 128, 0.4);
26    }
27    .metric-label { 
28        font-size: 11px; 
29        text-transform: uppercase; 
30        letter-spacing: 1.5px; 
31        color: #888888; 
32        margin-bottom: 8px; 
33        font-weight: 600;
34    }
35    .metric-value { 
36        font-size: 28px; 
37        font-weight: 300; 
38        letter-spacing: -0.5px; 
39    }
40    .pos-return { color: #5C946E !important; }
41    .neg-return { color: #C96464 !important; }
42    h1, h2, h3 { font-weight: 400 !important; letter-spacing: -0.5px; }
43</style>
44""", unsafe_allow_html=True)
45
46# --- CACHED DATA FETCHING ---
47# The @st.cache_data decorator saves the result for 1 hour (3600 seconds).
48# This prevents Yahoo from blocking the app due to too many requests!
49@st.cache_data(ttl=3600, show_spinner=False)
50def fetch_stock_data(ticker):
51    stock = yf.Ticker(ticker)
52    hist_max = pd.DataFrame()
53    
54    # 1. Fetch History (Our primary source of truth)
55    # We wrap this in a try-except because Streamlit Cloud frequently gets YFRateLimitErrors
56    try:
57        hist_max = stock.history(period="max")
58    except Exception:
59        pass # Ignore the crash, we will use the raw fallback below
60        
61    # 2. RAW HTTP FALLBACK: If yfinance is blocked, we fetch directly from Yahoo's backend
62    if hist_max is None or hist_max.empty:
63        try:
64            url = f"https://query2.finance.yahoo.com/v8/finance/chart/{ticker}?range=max&interval=1d"
65            headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36'}
66            res = requests.get(url, headers=headers, timeout=5)
67            
68            if res.status_code == 200:
69                data = res.json()
70                timestamps = data['chart']['result'][0]['timestamp']
71                closes = data['chart']['result'][0]['indicators']['quote'][0]['close']
72                # Create a perfectly formatted dataframe from the raw data
73                hist_max = pd.DataFrame({'Close': closes}, index=pd.to_datetime(timestamps, unit='s', utc=True))
74        except Exception:
75            pass
76
77    # If even the fallback fails, return a polite error instead of a crashed app
78    if hist_max is None or hist_max.empty:
79        return False, f"Data completely blocked by Yahoo for {ticker}. Please try again later.", None, None, None, None
80        
81    ipo_date = hist_max.index.min().date()
82    
83    # 3. Fetch Info (Silently catch rate limits)
84    try:
85        stock_info = stock.info or {}
86    except Exception:
87        stock_info = {}
88        
89    # 4. Fetch Fast Info for backup Market Cap
90    try:
91        fast_mcap = stock.fast_info.get('marketCap', 0)
92    except Exception:
93        fast_mcap = 0
94        
95    return True, "Success", hist_max, stock_info, ipo_date, fast_mcap
96
97@st.cache_data(ttl=86400, show_spinner=False) # Cache funds for 24 hours
98def fetch_funds(ticker):
99    try:
100        return yf.Ticker(ticker).mutualfund_holders
101    except Exception:
102        return None
103
104# --- METRICS CALCULATOR ---
105def calculate_metrics(hist_max):
106    current_year = datetime.now().year
107    if hist_max is None or hist_max.empty:
108        return 0, 0, "N/A", "N/A"
109        
110    current_price = float(hist_max['Close'].iloc[-1])
111    prev_close = float(hist_max['Close'].iloc[-2]) if len(hist_max) > 1 else current_price
112    
113    # YTD
114    ytd_data = hist_max[hist_max.index.year == current_year]
115    if not ytd_data.empty:
116        first_ytd = float(ytd_data['Close'].iloc[0])
117        ytd_val = ((current_price - first_ytd) / first_ytd) * 100
118        ytd_return = f"{ytd_val:+.2f}%"
119    else:
120        ytd_return = "N/A"
121        
122    # 1-Year (Handles timezone differences safely)
123    now_ts = pd.Timestamp.now(tz=hist_max.index.tz) if hasattr(hist_max.index, 'tz') else pd.Timestamp.now()
124    one_year_ago = now_ts - pd.Timedelta(days=365)
125    past_data = hist_max[hist_max.index <= one_year_ago]
126    
127    if not past_data.empty:
128        first_1y = float(past_data['Close'].iloc[-1])
129        one_yr_val = ((current_price - first_1y) / first_1y) * 100
130        one_yr_return = f"{one_yr_val:+.2f}%" if len(hist_max) >= 250 else f"{one_yr_val:+.2f}% (Since IPO)"
131    else:
132        first_ipo = float(hist_max['Close'].iloc[0])
133        one_yr_val = ((current_price - first_ipo) / first_ipo) * 100
134        one_yr_return = f"{one_yr_val:+.2f}% (Since IPO)"
135
136    return current_price, prev_close, ytd_return, one_yr_return
137
138# --- UI LAYOUT ---
139st.title("Post-IPO Catalyst & Flow Tracker")
140st.markdown("<p style='color: #888; font-size: 16px; font-weight: 300;'>Predictive Index Inclusion & IPO Lock-up Mapping</p>", unsafe_allow_html=True)
141st.write("")
142
143# Inputs
144col_search, col_override = st.columns([2, 1])
145with col_search:
146    ticker_input = st.text_input("Enter Ticker (e.g. EIKN, ARM, AAPL)", "")
147with col_override:
148    sector_override = st.selectbox(
149        "Sector (Use if Auto-Detect fails)", 
150        ["Auto-Detect", "Healthcare / Biotech", "Technology / Growth", "Other"]
151    )
152
153if ticker_input:
154    ticker = ticker_input.upper().strip()
155    with st.spinner(f"Pulling optimized market data for {ticker}..."):
156        
157        # Call our new, super-fast cached functions!
158        success, msg, hist_max, stock_info, ipo_date, fast_mcap = fetch_stock_data(ticker)
159        
160        if not success:
161            st.error(msg)
162        else:
163            # Profile Data
164            sector = stock_info.get('sector', 'Unknown')
165            industry = stock_info.get('industry', 'Unknown')
166            
167            display_sector = sector
168            if sector == 'Unknown' and sector_override != "Auto-Detect":
169                display_sector = f"Manual: {sector_override}"
170            
171            mcap = stock_info.get('marketCap', fast_mcap)
172            mcap_str = f"${mcap / 1e9:.2f}B" if mcap else "Unknown"
173            
174            days_public = (datetime.now().date() - ipo_date).days
175            is_mature = days_public > 365
176            status_badge = "Mature Company" if is_mature else "Recent IPO"
177
178            st.write("---")
179            
180            # Top Row: Info & Prices
181            col1, col2 = st.columns([1, 2])
182            with col1:
183                st.subheader(f"{ticker} Profile")
184                st.caption(stock_info.get('shortName', 'Company Name'))
185                st.markdown(f"**Status:** {status_badge}")
186                st.markdown(f"**Sector:** {display_sector}")
187                st.markdown(f"**Industry:** {industry}")
188                st.markdown(f"**Est. Market Cap:** {mcap_str}")
189                
190            with col2:
191                st.subheader("Price & Performance")
192                cp, pc, ytd, oyr = calculate_metrics(hist_max)
193                
194                m1, m2, m3, m4 = st.columns(4)
195                m1.metric("Current Price", f"${cp:.2f}" if cp else "N/A", f"{cp - pc:+.2f}" if cp and pc else None)
196                m2.metric("Previous Close", f"${pc:.2f}" if pc else "N/A")
197                m3.metric("YTD Return", ytd)
198                m4.metric("1-Year Return", oyr)
199
200            st.write("---")
201
202            # Middle Row: Deadlines
203            st.subheader("Mechanical & Regulatory Deadlines")
204            st.write("")
205            
206            deadlines = {
207                "IPO Pricing / First Trade": ipo_date,
208                "Quiet Period (T+25)": ipo_date + timedelta(days=25),
209                "Lock-Up Expiry (T+180)": ipo_date + timedelta(days=180)
210            }
211            
212            d_cols = st.columns(3)
213            for idx, (event, date) in enumerate(deadlines.items()):
214                passed = date < datetime.now().date()
215                status = "Passed" if passed else "Upcoming"
216                color = "#888888" if passed else "#5C946E"
217                
218                with d_cols[idx]:
219                    st.markdown(f"""
220                    <div class="metric-card">
221                        <div class="metric-label">{event}</div>
222                        <div class="metric-value">{date.strftime('%b %d, %Y')}</div>
223                        <div style="color: {color}; font-size: 11px; font-weight: 600; letter-spacing: 1px; text-transform: uppercase; margin-top: 12px;">{status}</div>
224                    </div>
225                    """, unsafe_allow_html=True)
226
227            st.write("---")
228
229            # Bottom Row: Index Logic
230            if is_mature:
231                st.subheader("Top Passive Institutional Holders")
232                st.markdown(f"<p style='color: #888; font-size: 14px;'>{ticker} has been public for >1 year. Mechanical lock-ups are irrelevant. The funds listed below control the daily passive flows.</p>", unsafe_allow_html=True)
233                
234                # Fetch cached funds
235                funds = fetch_funds(ticker)
236                
237                if funds is not None and not funds.empty:
238                    funds_clean = funds.head(5)[['Holder', 'pctHeld']]
239                    funds_clean['pctHeld'] = (funds_clean['pctHeld'] * 100).round(2).astype(str) + '%'
240                    funds_clean.columns = ['Fund Name', '% of Float Owned']
241                    st.table(funds_clean)
242                else:
243                    st.warning("Fund data temporarily unavailable due to rate limits from data provider.")
244            else:
245                st.subheader("Predictive Index Inclusion Targets")
246                st.write("")
247                
248                inclusions = []
249                ipo_month = ipo_date.month
250                
251                if ipo_month <= 4: 
252                    inclusions.append({"Index": "Russell 2000/3000", "Target": "Late June", "Prob": "High", "Rationale": "Eligible for the June Reconstitution."})
253                elif ipo_month <= 10: 
254                    inclusions.append({"Index": "Russell 2000/3000", "Target": "Dec 11", "Prob": "High", "Rationale": "Eligible for the December Semi-Annual Reconstitution."})
255                
256                inclusions.append({"Index": "CRSP US Total Market (VTI)", "Target": "Next Quarterly Rebalance", "Prob": "High", "Rationale": "Quarterly rebalance inclusion."})
257                inclusions.append({"Index": "MSCI USA IMI", "Target": "Next Index Review", "Prob": "High" if mcap >= 1e9 else "Medium", "Rationale": "Quarterly/Semi-Annual reviews based on liquidity/cap."})
258                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."})
259                
260                is_biotech = False
261                is_tech = False
262                
263                if sector_override == "Healthcare / Biotech":
264                    is_biotech = True
265                elif sector_override == "Technology / Growth":
266                    is_tech = True
267                elif sector_override == "Auto-Detect":
268                    is_biotech = sector == 'Healthcare' or 'Biotech' in industry or 'Pharmaceutical' in industry
269                    is_tech = sector in ['Technology', 'Communication Services', 'Consumer Discretionary']
270
271                if is_biotech:
272                    inclusions.append({"Index": "S&P Biotech (XBI)", "Target": "Next Quarterly Rebalance", "Prob": "High", "Rationale": "Requires 1-2 months seasoning."})
273                    inclusions.append({"Index": "Nasdaq Biotech (NBI)", "Target": "December (Annual)", "Prob": "High", "Rationale": "Annual December reconstitution."})
274                elif is_tech or (not is_biotech and sector_override == "Auto-Detect"):
275                    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."})
276
277                st.table(pd.DataFrame(inclusions))