mirror of
https://github.com/shawnkim1997/All-in-one-Financial-Analysis.git
synced 2026-08-15 03:18:07 +00:00
165 lines
5.6 KiB
Python
165 lines
5.6 KiB
Python
"""
|
|||
|
|
Portfolio data layer — fetch current prices, earnings calendar, dividends, sector info, news for portfolio holdings.
|
||
|
|
"""
|
||
|
|
import streamlit as st
|
||
|
|
import pandas as pd
|
||
|
|
from datetime import datetime, timedelta
|
||
|
|
|
||
|
|
try:
|
||
|
|
import yfinance as yf
|
||
|
|
except ImportError:
|
||
|
|
yf = None
|
||
|
|
|
||
|
|
|
||
|
|
@st.cache_data(ttl=120)
|
||
|
|
def get_portfolio_prices(tickers: tuple) -> dict:
|
||
|
|
"""Fetch current price, previous close, and day change for each ticker.
|
||
|
|
Returns {ticker: {"price": float, "prev_close": float, "change_pct": float}}
|
||
|
|
"""
|
||
|
|
if not yf or not tickers:
|
||
|
|
return {}
|
||
|
|
result = {}
|
||
|
|
for sym in tickers:
|
||
|
|
try:
|
||
|
|
t = yf.Ticker(sym)
|
||
|
|
info = t.info or {}
|
||
|
|
price = info.get("regularMarketPrice") or info.get("currentPrice")
|
||
|
|
prev = info.get("regularMarketPreviousClose") or info.get("previousClose")
|
||
|
|
if price and prev and prev != 0:
|
||
|
|
change = (price - prev) / prev * 100
|
||
|
|
else:
|
||
|
|
change = 0.0
|
||
|
|
result[sym] = {"price": price, "prev_close": prev, "change_pct": round(change, 2)}
|
||
|
|
except Exception:
|
||
|
|
result[sym] = {"price": None, "prev_close": None, "change_pct": 0.0}
|
||
|
|
return result
|
||
|
|
|
||
|
|
|
||
|
|
@st.cache_data(ttl=300)
|
||
|
|
def get_sector_allocation(tickers: tuple) -> dict:
|
||
|
|
"""Return {ticker: sector} for pie chart. Uses yfinance .info."""
|
||
|
|
if not yf or not tickers:
|
||
|
|
return {}
|
||
|
|
result = {}
|
||
|
|
for sym in tickers:
|
||
|
|
try:
|
||
|
|
info = yf.Ticker(sym).info or {}
|
||
|
|
result[sym] = info.get("sector") or info.get("sectorDisp") or "Other"
|
||
|
|
except Exception:
|
||
|
|
result[sym] = "Other"
|
||
|
|
return result
|
||
|
|
|
||
|
|
|
||
|
|
@st.cache_data(ttl=600)
|
||
|
|
def get_earnings_calendar(tickers: tuple) -> list:
|
||
|
|
"""Return list of upcoming earnings: [{"ticker", "name", "date", "days_until"}]."""
|
||
|
|
if not yf or not tickers:
|
||
|
|
return []
|
||
|
|
events = []
|
||
|
|
now = datetime.now()
|
||
|
|
for sym in tickers:
|
||
|
|
try:
|
||
|
|
t = yf.Ticker(sym)
|
||
|
|
cal = t.calendar
|
||
|
|
if cal is None:
|
||
|
|
continue
|
||
|
|
# yfinance returns dict or DataFrame
|
||
|
|
if isinstance(cal, pd.DataFrame):
|
||
|
|
if "Earnings Date" in cal.index:
|
||
|
|
dates = cal.loc["Earnings Date"]
|
||
|
|
ed = pd.Timestamp(dates.iloc[0]) if len(dates) > 0 else None
|
||
|
|
else:
|
||
|
|
continue
|
||
|
|
elif isinstance(cal, dict):
|
||
|
|
ed_val = cal.get("Earnings Date")
|
||
|
|
if isinstance(ed_val, list) and ed_val:
|
||
|
|
ed = pd.Timestamp(ed_val[0])
|
||
|
|
elif ed_val:
|
||
|
|
ed = pd.Timestamp(ed_val)
|
||
|
|
else:
|
||
|
|
continue
|
||
|
|
else:
|
||
|
|
continue
|
||
|
|
if ed and ed >= pd.Timestamp(now):
|
||
|
|
delta = (ed - pd.Timestamp(now)).days
|
||
|
|
name = (t.info or {}).get("shortName", sym)
|
||
|
|
events.append({
|
||
|
|
"ticker": sym, "name": name,
|
||
|
|
"date": ed.strftime("%m/%d"), "days_until": delta,
|
||
|
|
})
|
||
|
|
except Exception:
|
||
|
|
continue
|
||
|
|
events.sort(key=lambda x: x["days_until"])
|
||
|
|
return events
|
||
|
|
|
||
|
|
|
||
|
|
@st.cache_data(ttl=600)
|
||
|
|
def get_dividend_schedule(tickers: tuple) -> list:
|
||
|
|
"""Return upcoming dividend info: [{"ticker", "name", "ex_date", "amount", "yield_pct"}]."""
|
||
|
|
if not yf or not tickers:
|
||
|
|
return []
|
||
|
|
divs = []
|
||
|
|
for sym in tickers:
|
||
|
|
try:
|
||
|
|
t = yf.Ticker(sym)
|
||
|
|
info = t.info or {}
|
||
|
|
div_rate = info.get("dividendRate")
|
||
|
|
div_yield = info.get("dividendYield")
|
||
|
|
ex_date = info.get("exDividendDate")
|
||
|
|
if not div_rate and not div_yield:
|
||
|
|
continue
|
||
|
|
name = info.get("shortName", sym)
|
||
|
|
ex_str = ""
|
||
|
|
if ex_date:
|
||
|
|
try:
|
||
|
|
ex_dt = datetime.fromtimestamp(ex_date) if isinstance(ex_date, (int, float)) else ex_date
|
||
|
|
ex_str = ex_dt.strftime("%m/%d/%Y") if hasattr(ex_dt, "strftime") else str(ex_date)
|
||
|
|
except Exception:
|
||
|
|
ex_str = str(ex_date)
|
||
|
|
divs.append({
|
||
|
|
"ticker": sym, "name": name, "ex_date": ex_str,
|
||
|
|
"amount": f"${div_rate:.2f}" if div_rate else "N/A",
|
||
|
|
"yield_pct": f"{div_yield * 100:.2f}%" if div_yield else "N/A",
|
||
|
|
})
|
||
|
|
except Exception:
|
||
|
|
continue
|
||
|
|
return divs
|
||
|
|
|
||
|
|
|
||
|
|
@st.cache_data(ttl=300)
|
||
|
|
def get_portfolio_news(tickers: tuple, max_per_ticker: int = 3) -> list:
|
||
|
|
"""Fetch recent news for portfolio tickers via yfinance."""
|
||
|
|
if not yf or not tickers:
|
||
|
|
return []
|
||
|
|
all_news = []
|
||
|
|
for sym in tickers:
|
||
|
|
try:
|
||
|
|
t = yf.Ticker(sym)
|
||
|
|
news_list = t.news or []
|
||
|
|
for n in news_list[:max_per_ticker]:
|
||
|
|
all_news.append({
|
||
|
|
"ticker": sym,
|
||
|
|
"title": n.get("title", ""),
|
||
|
|
"publisher": n.get("publisher", ""),
|
||
|
|
"link": n.get("link", ""),
|
||
|
|
"published": n.get("providerPublishTime", 0),
|
||
|
|
})
|
||
|
|
except Exception:
|
||
|
|
continue
|
||
|
|
all_news.sort(key=lambda x: x.get("published", 0), reverse=True)
|
||
|
|
return all_news[:20]
|
||
|
|
|
||
|
|
|
||
|
|
@st.cache_data(ttl=300)
|
||
|
|
def get_sparkline_data(ticker: str, period: str = "1y") -> list:
|
||
|
|
"""Return list of close prices for sparkline chart."""
|
||
|
|
if not yf or not ticker:
|
||
|
|
return []
|
||
|
|
try:
|
||
|
|
hist = yf.Ticker(ticker).history(period=period)
|
||
|
|
if hist is not None and not hist.empty and "Close" in hist.columns:
|
||
|
|
return hist["Close"].tolist()
|
||
|
|
except Exception:
|
||
|
|
pass
|
||
|
|
return []
|