"""Insider transactions tab rendered as a client-side HTML surface.""" from html import escape as _esc import pandas as pd import streamlit as st import streamlit.components.v1 as components from services.data_service import ( get_company_info, get_insider_transactions, get_latest_price, ) _XMAP = {"NYQ": "NYSE", "NMS": "NASDAQ", "NGM": "NASDAQ", "NCM": "NASDAQ", "ASE": "AMEX"} def _classify(text: str) -> str: t = str(text or "").lower() if any(k in t for k in ("sale", "sold", "disposition")): return "Sell" if any(k in t for k in ("purchase", "bought", "acquisition", "grant", "award", "exercise")): return "Buy" return "Other" def render_insiders(ticker: str): import json as _json info = get_company_info(ticker) or {} price = get_latest_price(ticker) df = get_insider_transactions(ticker) if df is None or df.empty: st.info("No insider transaction data available for this ticker.") return df = df.copy() if "Start Date" in df.columns: df["_date"] = pd.to_datetime(df["Start Date"], errors="coerce") else: df["_date"] = pd.NaT def _num(val): try: n = float(val) except (TypeError, ValueError): return None if pd.isna(n): return None return n rows = [] for _, row in df.iterrows(): dt = row.get("_date") date_str = dt.strftime("%Y-%m-%d") if pd.notna(dt) else None month_str = dt.strftime("%Y-%m") if pd.notna(dt) else None shares_num = _num(row.get("Shares")) if shares_num is not None and abs(shares_num - round(shares_num)) < 1e-9: shares_num = int(round(shares_num)) value_num = _num(row.get("Value")) rows.append( { "date": date_str, "month": month_str, "insider": str(row.get("Insider")) if pd.notna(row.get("Insider")) else None, "position": str(row.get("Position")) if pd.notna(row.get("Position")) else None, "direction": _classify(row.get("Text")), "shares": shares_num, "value": value_num, "ownership": str(row.get("Ownership")) if pd.notna(row.get("Ownership")) else None, "text": str(row.get("Text")) if pd.notna(row.get("Text")) else None, "transaction": str(row.get("Transaction")) if pd.notna(row.get("Transaction")) else None, } ) rows.sort(key=lambda r: (r.get("date") is not None, r.get("date") or ""), reverse=True) latest_date = rows[0].get("date") if rows else None n_rows = max(len(rows), 18) height = 1320 + n_rows * 32 def _safe_float(val): try: return float(val) except (TypeError, ValueError): return None current_price = info.get("currentPrice") or info.get("regularMarketPrice") or price prev_close = info.get("previousClose") cur_num = _safe_float(current_price) prev_num = _safe_float(prev_close) if cur_num is not None and prev_num is not None and prev_num > 0: chg_pct = (cur_num - prev_num) / prev_num * 100.0 chg_sign = "+" if chg_pct >= 0 else "" chg_arrow = "▲" if chg_pct >= 0 else "▼" chg_str = chg_arrow + " " + chg_sign + "{:.2f}%".format(chg_pct) chg_cls = "chg-pos" if chg_pct >= 0 else "chg-neg" else: chg_str = "—" chg_cls = "" exchange_raw = str(info.get("exchange") or "") exchange = _XMAP.get(exchange_raw, exchange_raw) or "—" co_name = _esc(info.get("longName") or info.get("shortName") or ticker.upper()) price_str = "${:,.2f}".format(cur_num) if cur_num is not None else "—" meta = {"ticker": ticker.upper(), "latest_date": latest_date, "transactions": len(rows)} rows_js = "const INSIDERS=" + _json.dumps(rows) + ";" meta_js = "const INSIDER_META=" + _json.dumps(meta) + ";" plotly_cdn = "" _ROOT = ( "" ) fonts_link = ( "" "" ) _IN_CSS = """""" ctx_html = ( '
Recent insider transactions for ' + _esc(ticker.upper()) + ", grouped by direction, month, and participant. Use the controls below to isolate buy or sell activity and inspect the underlying filings feed from Yahoo Finance via yfinance.
" + "