diff options
| author | Tyler Hoang <tyler@tylerhoang.xyz> | 2026-06-09 19:19:44 -0700 |
|---|---|---|
| committer | Tyler Hoang <tyler@tylerhoang.xyz> | 2026-06-09 19:19:44 -0700 |
| commit | d5e321089b6b82f5425c2b3fae142db5223ce599 (patch) | |
| tree | afdb1e7753938d00bb0a7adb2275525c76482dd9 | |
| parent | 9e2a23855d29d9763044b8a0a9e5f326cfcad878 (diff) | |
Add Insiders tab with diverging bar chart and transaction table
- Backend: get_insider_transactions() in data_service (yfinance, 1hr TTL)
- Backend: InsidersResponse schema + GET /api/tickers/{symbol}/insiders
- Frontend: InsidersPage with 4-KPI strip, SVG diverging bar chart (),
psm-insider transaction rows, pill filter toggles, shimmer skeleton,
empty/error states
- Enable Insiders nav item (remove disabled flag)
| -rw-r--r-- | backend/app/main.py | 7 | ||||
| -rw-r--r-- | backend/app/schemas.py | 28 | ||||
| -rw-r--r-- | backend/app/services/data_service.py | 96 | ||||
| -rw-r--r-- | frontend/app/page.tsx | 8 | ||||
| -rw-r--r-- | frontend/components/prism/InsidersPage.tsx | 507 | ||||
| -rw-r--r-- | frontend/lib/api.ts | 9 | ||||
| -rw-r--r-- | frontend/lib/overview.ts | 2 | ||||
| -rw-r--r-- | frontend/types/api.ts | 28 |
8 files changed, 681 insertions, 4 deletions
diff --git a/backend/app/main.py b/backend/app/main.py index 7e02cbe..386a695 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -9,7 +9,7 @@ from fastapi import FastAPI, HTTPException, Query, status from fastapi.middleware.cors import CORSMiddleware from app.db import watchlist -from app.schemas import FinancialsResponse, HistoryPoint, MarketIndex, RatiosResponse, SearchResult, TickerOverview, ValuationResponse, WatchlistResponse +from app.schemas import FinancialsResponse, HistoryPoint, InsidersResponse, MarketIndex, RatiosResponse, SearchResult, TickerOverview, ValuationResponse, WatchlistResponse from app.services import data_service load_dotenv() @@ -80,6 +80,11 @@ def ticker_ratios(symbol: str) -> dict: return data_service.get_ratios(symbol) +@app.get("/api/tickers/{symbol}/insiders", response_model=InsidersResponse) +def ticker_insiders(symbol: str) -> dict: + return data_service.get_insider_transactions(symbol) + + @app.get("/api/watchlist", response_model=WatchlistResponse) def get_watchlist() -> dict: items = [] diff --git a/backend/app/schemas.py b/backend/app/schemas.py index c84b4c6..b1d934a 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -206,5 +206,33 @@ class RatiosResponse(BaseModel): dividend_payout: RatioPoint +class InsiderTransaction(BaseModel): + date: str | None = None + insider: str + position: str + direction: Literal["buy", "sell", "other"] + shares: int | None = None + value: float | None = None + + +class InsiderMonthPoint(BaseModel): + month: str + buy: float + sell: float + + +class InsiderSummary(BaseModel): + buy_count: int + sell_count: int + buy_value: float + sell_value: float + + +class InsidersResponse(BaseModel): + summary: InsiderSummary + monthly_chart: list[InsiderMonthPoint] = Field(default_factory=list) + transactions: list[InsiderTransaction] = Field(default_factory=list) + + class ErrorResponse(BaseModel): detail: str diff --git a/backend/app/services/data_service.py b/backend/app/services/data_service.py index 9662227..108d816 100644 --- a/backend/app/services/data_service.py +++ b/backend/app/services/data_service.py @@ -32,6 +32,7 @@ VALUATION_CACHE = TTLCache(maxsize=128, ttl=3600) HIST_RATIOS_CACHE: TTLCache = TTLCache(maxsize=128, ttl=3600) RATIOS_ENDPOINT_CACHE: TTLCache = TTLCache(maxsize=128, ttl=3600) SECTOR_BENCHMARK_CACHE: TTLCache = TTLCache(maxsize=128, ttl=3600) +INSIDERS_CACHE: TTLCache = TTLCache(maxsize=256, ttl=3600) PERIODS = {"1m", "3m", "6m", "1y", "2y", "5y"} YF_PERIOD_MAP = {"1m": "1mo", "3m": "3mo", "6m": "6mo", "1y": "1y", "2y": "2y", "5y": "5y"} @@ -1770,3 +1771,98 @@ def get_ticker_overview(symbol: str) -> dict[str, Any] | None: "sources": field_sources, }, } + + +def _classify_insider(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" + + +@cached(INSIDERS_CACHE) +def get_insider_transactions(symbol: str) -> dict: + sym = normalize_symbol(symbol) + try: + t = yf.Ticker(sym) + df = t.insider_transactions + except Exception: + df = None + + if df is None or (hasattr(df, "empty") and df.empty): + return { + "summary": {"buy_count": 0, "sell_count": 0, "buy_value": 0.0, "sell_value": 0.0}, + "monthly_chart": [], + "transactions": [], + } + + df = df.copy() + df["direction"] = df["Text"].apply(_classify_insider) + + def _to_dt(val: object) -> "pd.Timestamp | None": + try: + return pd.to_datetime(val) + except Exception: + return None + + df["_date"] = df["Start Date"].apply(_to_dt) + + cutoff = pd.Timestamp.now() - pd.Timedelta(days=180) + recent = df[df["_date"] >= cutoff] + + def _total_value(subset: "pd.DataFrame") -> float: + try: + return float(subset["Value"].dropna().astype(float).sum()) + except Exception: + return 0.0 + + buys = recent[recent["direction"] == "buy"] + sells = recent[recent["direction"] == "sell"] + + summary = { + "buy_count": int(len(buys)), + "sell_count": int(len(sells)), + "buy_value": _total_value(buys), + "sell_value": _total_value(sells), + } + + # Monthly chart: last 6 months, buys positive, sells negative (in $M) + monthly: dict[str, dict[str, float]] = {} + for _, row in recent.iterrows(): + dt = row["_date"] + if dt is None or pd.isna(dt): + continue + key = dt.strftime("%Y-%m") + monthly.setdefault(key, {"buy": 0.0, "sell": 0.0}) + try: + val = float(row["Value"]) if pd.notna(row["Value"]) else 0.0 + except (TypeError, ValueError): + val = 0.0 + if row["direction"] in ("buy", "sell"): + monthly[key][row["direction"]] += val + + monthly_chart = [ + {"month": m, "buy": monthly[m]["buy"] / 1e6, "sell": monthly[m]["sell"] / 1e6} + for m in sorted(monthly.keys()) + ] + + # All transactions (newest first) + transactions = [] + for _, row in df.sort_values("_date", ascending=False).iterrows(): + dt = row["_date"] + transactions.append({ + "date": dt.strftime("%Y-%m-%d") if dt is not None and not pd.isna(dt) else None, + "insider": str(row.get("Insider", "") or ""), + "position": str(row.get("Position", "") or ""), + "direction": row["direction"], + "shares": int(row["Shares"]) if pd.notna(row.get("Shares")) else None, + "value": _safe_float(row.get("Value")), + }) + + return { + "summary": summary, + "monthly_chart": monthly_chart, + "transactions": transactions, + } diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index 175f072..e1f16ee 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -5,6 +5,7 @@ import Image from "next/image"; import { useRouter, useSearchParams } from "next/navigation"; import { AppShell } from "@/components/prism/AppShell"; import { FinancialsPage } from "@/components/prism/FinancialsPage"; +import { InsidersPage } from "@/components/prism/InsidersPage"; import { ValuationPage } from "@/components/prism/ValuationPage"; import { OptionsPage } from "@/components/prism/options/OptionsPage"; import { ChartCard } from "@/components/prism/ChartCard"; @@ -346,6 +347,13 @@ function OverviewClient() { isSaved={isSaved} onToggleWatchlist={addOrRemoveCurrentTicker} /> + ) : tab === "insiders" ? ( + <InsidersPage + ticker={selectedTicker} + overview={overview} + isSaved={isSaved} + onToggleWatchlist={addOrRemoveCurrentTicker} + /> ) : ( <> <TickerHeader overview={overview} onToggleWatchlist={addOrRemoveCurrentTicker} isSaved={isSaved} /> diff --git a/frontend/components/prism/InsidersPage.tsx b/frontend/components/prism/InsidersPage.tsx new file mode 100644 index 0000000..c0d1363 --- /dev/null +++ b/frontend/components/prism/InsidersPage.tsx @@ -0,0 +1,507 @@ +"use client"; +import { useEffect, useRef, useState } from "react"; +import { api } from "@/lib/api"; +import { buildKpis } from "@/lib/overview"; +import { fmtLarge, fmtNumber } from "@/lib/format"; +import { KPIStrip } from "@/components/prism/KPIStrip"; +import { TickerHeader } from "@/components/prism/TickerHeader"; +import type { InsidersResponse, InsiderTransaction, TickerOverview } from "@/types/api"; + +type InsidersState = "loading" | "ready" | "error" | "empty"; +type Filter = "all" | "buy" | "sell"; + +type Props = { + ticker: string; + overview: TickerOverview; + isSaved: boolean; + onToggleWatchlist: () => void; +}; + +// ── Skeleton ────────────────────────────────────────────────────────────────── + +function SkeletonBlock({ height, className = "" }: { height: number; className?: string }) { + return ( + <div + className={className} + style={{ + height, + borderRadius: "var(--r-3)", + background: "linear-gradient(90deg, var(--ink-2, #181d26) 25%, var(--ink-3, #1e232e) 50%, var(--ink-2, #181d26) 75%)", + backgroundSize: "200% 100%", + animation: "psm-shimmer 1.4s ease infinite", + }} + /> + ); +} + +function InsidersSkeleton() { + return ( + <> + {/* KPI row */} + <div style={{ display: "grid", gridTemplateColumns: "repeat(4, 1fr)", gap: "var(--sp-4)", marginBottom: "var(--sp-5)" }}> + {[0, 1, 2, 3].map((i) => <SkeletonBlock key={i} height={64} />)} + </div> + {/* Chart card */} + <div className="psm-card" style={{ marginBottom: "var(--sp-4)" }}> + <SkeletonBlock height={220} /> + </div> + {/* Table card */} + <div className="psm-card"> + <SkeletonBlock height={32} className="" /> + <div style={{ marginTop: "var(--sp-4)", display: "flex", flexDirection: "column", gap: "var(--sp-3)" }}> + {[0, 1, 2, 3, 4].map((i) => <SkeletonBlock key={i} height={44} />)} + </div> + </div> + </> + ); +} + +// ── Summary KPI strip ───────────────────────────────────────────────────────── + +function InsiderKPIs({ summary }: { summary: InsidersResponse["summary"] }) { + const items = [ + { label: "BUY TRANSACTIONS", value: String(summary.buy_count), tone: "pos" as const }, + { label: "TOTAL BOUGHT", value: fmtLarge(summary.buy_value), tone: "pos" as const }, + { label: "SELL TRANSACTIONS", value: String(summary.sell_count), tone: "neg" as const }, + { label: "TOTAL SOLD", value: fmtLarge(summary.sell_value), tone: "neg" as const }, + ]; + + return ( + <div + style={{ + display: "grid", + gridTemplateColumns: "repeat(4, 1fr)", + gap: "var(--sp-4)", + marginBottom: "var(--sp-5)", + }} + > + {items.map((item) => ( + <div + key={item.label} + className="psm-card" + style={{ padding: "var(--sp-4) var(--sp-5)" }} + > + <div + style={{ + fontFamily: "var(--font-sans)", + fontSize: "var(--fs-11)", + color: "var(--fg-3)", + letterSpacing: "0.08em", + textTransform: "uppercase", + marginBottom: "var(--sp-2)", + }} + > + {item.label} + </div> + <div + style={{ + fontFamily: "var(--font-mono)", + fontSize: "var(--fs-20)", + fontVariantNumeric: "tabular-nums", + color: item.tone === "pos" ? "var(--positive)" : "var(--negative)", + }} + > + {item.value || "—"} + </div> + </div> + ))} + </div> + ); +} + +// ── Monthly chart (SVG diverging bars) ─────────────────────────────────────── + +function MonthlyChart({ data }: { data: InsidersResponse["monthly_chart"] }) { + const containerRef = useRef<HTMLDivElement>(null); + const [width, setWidth] = useState(600); + + useEffect(() => { + if (!containerRef.current) return; + const ro = new ResizeObserver((entries) => { + setWidth(entries[0].contentRect.width); + }); + ro.observe(containerRef.current); + setWidth(containerRef.current.offsetWidth); + return () => ro.disconnect(); + }, []); + + if (!data.length) { + return ( + <div + style={{ + textAlign: "center", + padding: "var(--sp-8) 0", + fontFamily: "var(--font-sans)", + fontSize: "var(--fs-13)", + color: "var(--fg-3)", + }} + > + No activity in the last 6 months + </div> + ); + } + + const PAD_L = 52; + const PAD_R = 16; + const PAD_T = 16; + const PAD_B = 28; + const H = 220; + const chartW = Math.max(width - PAD_L - PAD_R, 10); + const chartH = H - PAD_T - PAD_B; + + // max absolute value for symmetric axis + const maxVal = Math.max(...data.map((d) => Math.max(d.buy, d.sell)), 0.01); + const axisMax = Math.ceil(maxVal * 1.15 * 10) / 10 || 1; + + const barW = Math.max(Math.floor((chartW / data.length) * 0.6), 4); + const barGap = chartW / data.length; + + const yScale = (v: number) => (v / axisMax) * (chartH / 2); + const midY = PAD_T + chartH / 2; + + // Y axis ticks + const ticks = [axisMax, axisMax / 2, 0, -axisMax / 2, -axisMax]; + + return ( + <div ref={containerRef} style={{ width: "100%" }}> + <svg + width={width} + height={H} + style={{ display: "block", overflow: "visible" }} + > + {/* Y axis ticks + labels */} + {ticks.map((t) => { + const y = midY - yScale(t); + const label = t === 0 ? "0" : `${t > 0 ? "+" : ""}${Math.abs(t).toFixed(1)}M`; + return ( + <g key={t}> + <line + x1={PAD_L - 4} + x2={PAD_L + chartW} + y1={y} + y2={y} + stroke={t === 0 ? "var(--line-1)" : "var(--line-1)"} + strokeWidth={t === 0 ? 1.5 : 0.5} + strokeDasharray={t === 0 ? undefined : "2 3"} + opacity={t === 0 ? 1 : 0.5} + /> + <text + x={PAD_L - 8} + y={y + 4} + textAnchor="end" + fontFamily="var(--font-mono)" + fontSize={10} + fill="var(--fg-3)" + > + {label} + </text> + </g> + ); + })} + + {/* Bars */} + {data.map((d, i) => { + const cx = PAD_L + i * barGap + barGap / 2; + const buyH = yScale(d.buy); + const sellH = yScale(d.sell); + + return ( + <g key={d.month}> + {/* Buy bar (up) */} + {d.buy > 0 && ( + <rect + x={cx - barW / 2} + y={midY - buyH} + width={barW} + height={buyH} + fill="var(--positive)" + opacity={0.85} + rx={1} + /> + )} + {/* Sell bar (down) */} + {d.sell > 0 && ( + <rect + x={cx - barW / 2} + y={midY} + width={barW} + height={sellH} + fill="var(--negative)" + opacity={0.85} + rx={1} + /> + )} + {/* Month label */} + <text + x={cx} + y={H - 6} + textAnchor="middle" + fontFamily="var(--font-mono)" + fontSize={10} + fill="var(--fg-3)" + > + {d.month.slice(5)} + </text> + </g> + ); + })} + </svg> + + {/* Legend */} + <div + style={{ + display: "flex", + gap: "var(--sp-5)", + justifyContent: "flex-end", + marginTop: "var(--sp-2)", + }} + > + {[ + { color: "var(--positive)", label: "Buys" }, + { color: "var(--negative)", label: "Sells" }, + ].map(({ color, label }) => ( + <div + key={label} + style={{ + display: "flex", + alignItems: "center", + gap: "var(--sp-2)", + fontFamily: "var(--font-sans)", + fontSize: "var(--fs-12)", + color: "var(--fg-3)", + }} + > + <span + style={{ + display: "inline-block", + width: 8, + height: 8, + borderRadius: 1, + background: color, + }} + /> + {label} + </div> + ))} + </div> + </div> + ); +} + +// ── Transaction table ───────────────────────────────────────────────────────── + +function TransactionRow({ tx }: { tx: InsiderTransaction }) { + const isBuy = tx.direction === "buy"; + const isSell = tx.direction === "sell"; + + return ( + <div className="psm-insider"> + <div className="who"> + <span className="name">{tx.insider || "—"}</span> + <span className="role">{tx.position || "—"}</span> + </div> + <span + className={`pill ${isBuy ? "buy" : isSell ? "sell" : ""}`} + style={ + !isBuy && !isSell + ? { background: "var(--ink-2, #181d26)", color: "var(--fg-3)", border: "1px solid var(--line-1)" } + : undefined + } + > + {tx.direction.toUpperCase()} + </span> + <div style={{ textAlign: "right" }}> + <div className="val">{tx.value ? fmtLarge(tx.value) : "—"}</div> + <div + style={{ + fontFamily: "var(--font-mono)", + fontSize: "var(--fs-11)", + color: "var(--fg-3)", + fontVariantNumeric: "tabular-nums", + }} + > + {tx.shares ? `${fmtNumber(tx.shares, 0)} sh` : ""} + {tx.date ? ` · ${tx.date}` : ""} + </div> + </div> + </div> + ); +} + +function TransactionTable({ transactions }: { transactions: InsiderTransaction[] }) { + const [filter, setFilter] = useState<Filter>("all"); + + const filtered = + filter === "all" ? transactions : transactions.filter((t) => t.direction === filter); + + const FILTERS: { key: Filter; label: string }[] = [ + { key: "all", label: "ALL" }, + { key: "buy", label: "BUYS" }, + { key: "sell", label: "SELLS" }, + ]; + + return ( + <div> + {/* Filter toggles */} + <div + style={{ + display: "flex", + gap: "var(--sp-2)", + marginBottom: "var(--sp-4)", + }} + > + {FILTERS.map(({ key, label }) => ( + <button + key={key} + onClick={() => setFilter(key)} + className={`psm-tab${filter === key ? " active" : ""}`} + style={{ cursor: "pointer" }} + > + {label} + </button> + ))} + </div> + + {filtered.length === 0 ? ( + <div + style={{ + textAlign: "center", + padding: "var(--sp-8) 0", + fontFamily: "var(--font-sans)", + fontSize: "var(--fs-13)", + color: "var(--fg-3)", + }} + > + No transactions match filter + </div> + ) : ( + filtered.map((tx, i) => <TransactionRow key={i} tx={tx} />) + )} + </div> + ); +} + +// ── Page ────────────────────────────────────────────────────────────────────── + +export function InsidersPage({ ticker, overview, isSaved, onToggleWatchlist }: Props) { + const [data, setData] = useState<InsidersResponse | null>(null); + const [state, setState] = useState<InsidersState>("loading"); + const kpis = buildKpis(overview); + + useEffect(() => { + let cancelled = false; + setState("loading"); + setData(null); + + api + .insiders(ticker) + .then((res) => { + if (cancelled) return; + if (!res.transactions.length) { + setState("empty"); + } else { + setData(res); + setState("ready"); + } + }) + .catch(() => { + if (!cancelled) setState("error"); + }); + + return () => { + cancelled = true; + }; + }, [ticker]); + + return ( + <> + <TickerHeader overview={overview} onToggleWatchlist={onToggleWatchlist} isSaved={isSaved} /> + <KPIStrip items={kpis} /> + + <div style={{ padding: "0 var(--sp-1)" }}> + {state === "loading" && <InsidersSkeleton />} + + {state === "error" && ( + <div className="psm-card" style={{ textAlign: "center", padding: "var(--sp-8)" }}> + <div + style={{ + fontFamily: "var(--font-sans)", + fontSize: "var(--fs-14)", + color: "var(--fg-3)", + }} + > + Failed to load insider data + </div> + </div> + )} + + {state === "empty" && ( + <div className="psm-card" style={{ textAlign: "center", padding: "var(--sp-8)" }}> + <div + style={{ + fontFamily: "var(--font-sans)", + fontSize: "var(--fs-14)", + color: "var(--fg-3)", + }} + > + No insider activity reported + </div> + </div> + )} + + {state === "ready" && data && ( + <> + <InsiderKPIs summary={data.summary} /> + + <div className="psm-card" style={{ marginBottom: "var(--sp-4)" }}> + <div + style={{ + display: "flex", + justifyContent: "space-between", + alignItems: "baseline", + marginBottom: "var(--sp-4)", + }} + > + <span + style={{ + fontFamily: "var(--font-sans)", + fontSize: "var(--fs-11)", + color: "var(--fg-3)", + letterSpacing: "0.08em", + textTransform: "uppercase", + }} + > + Monthly Insider Activity — Last 6 Months ($M) + </span> + </div> + <MonthlyChart data={data.monthly_chart} /> + </div> + + <div className="psm-card"> + <div + style={{ + fontFamily: "var(--font-sans)", + fontSize: "var(--fs-11)", + color: "var(--fg-3)", + letterSpacing: "0.08em", + textTransform: "uppercase", + marginBottom: "var(--sp-4)", + }} + > + All Transactions + </div> + <TransactionTable transactions={data.transactions} /> + </div> + </> + )} + </div> + + <style>{` + @keyframes psm-shimmer { + 0% { background-position: 200% 0; } + 100% { background-position: -200% 0; } + } + `}</style> + </> + ); +} diff --git a/frontend/lib/api.ts b/frontend/lib/api.ts index 2c65ef2..84df997 100644 --- a/frontend/lib/api.ts +++ b/frontend/lib/api.ts @@ -1,4 +1,4 @@ -import type { FinancialsResponse, HistoryPoint, MarketIndex, RatiosResponse, SearchResult, TickerOverview, ValuationResponse, WatchlistResponse } from "@/types/api"; +import type { FinancialsResponse, HistoryPoint, InsidersResponse, MarketIndex, RatiosResponse, SearchResult, TickerOverview, ValuationResponse, WatchlistResponse } from "@/types/api"; const API_BASE = (process.env.NEXT_PUBLIC_API_BASE_URL || "").trim().replace(/\/$/, ""); @@ -63,5 +63,10 @@ export const api = { return request<RatiosResponse>( `/api/tickers/${encodeURIComponent(symbol)}/ratios` ); - } + }, + insiders(symbol: string) { + return request<InsidersResponse>( + `/api/tickers/${encodeURIComponent(symbol)}/insiders` + ); + }, }; diff --git a/frontend/lib/overview.ts b/frontend/lib/overview.ts index bb63e83..04f9c64 100644 --- a/frontend/lib/overview.ts +++ b/frontend/lib/overview.ts @@ -20,7 +20,7 @@ export const OVERVIEW_NAV_ITEMS: NavItem[] = [ { key: "financials", label: "Financials", icon: "ledger" }, { key: "valuation", label: "Valuation", icon: "dollar" }, { key: "options", label: "Options", icon: "window" }, - { key: "insiders", label: "Insiders", icon: "pulse", disabled: true }, +{ key: "insiders", label: "Insiders", icon: "pulse" }, { key: "filings", label: "Filings", icon: "folder", disabled: true }, { key: "news", label: "News", icon: "terminal", disabled: true } ]; diff --git a/frontend/types/api.ts b/frontend/types/api.ts index c8fc26b..c65d3d2 100644 --- a/frontend/types/api.ts +++ b/frontend/types/api.ts @@ -164,6 +164,34 @@ export type RatioPoint = { vs_sector: number | null; }; +export type InsiderTransaction = { + date: string | null; + insider: string; + position: string; + direction: "buy" | "sell" | "other"; + shares: number | null; + value: number | null; +}; + +export type InsiderMonthPoint = { + month: string; + buy: number; + sell: number; +}; + +export type InsiderSummary = { + buy_count: number; + sell_count: number; + buy_value: number; + sell_value: number; +}; + +export type InsidersResponse = { + summary: InsiderSummary; + monthly_chart: InsiderMonthPoint[]; + transactions: InsiderTransaction[]; +}; + export type RatiosResponse = { pe_ttm: RatioPoint; ev_ebitda: RatioPoint; |
