Files
All-in-one-Financial-Analysis/views/portfolio_widgets.py
shawnkim1997andClaude Opus 4.6 b2acda81ee feat: add Atlas Terminal — Next.js 14 + FastAPI full-stack migration
Complete migration from Streamlit to Next.js 14 App Router + FastAPI backend.

Frontend (Next.js 14):
- 10 pages: Overview, Research, Valuation, Technical, Markets, Earnings, News, Portfolio, Filings, Settings
- Terminal Noir dark theme with custom Tailwind config
- TradingView Lightweight Charts for candlestick/volume
- Valuation: DCF, Sensitivity Matrix, Monte Carlo, Tornado, Reverse DCF
- Financial Statements table with YoY growth badges and margin rows
- SEC EDGAR inline filing viewer with section tabs
- News split-view with iframe article embedding
- Technical Analysis with RSI, MACD, Bollinger, Fibonacci, Moving Averages
- Earnings beat/miss visualization
- AI Copilot chat panel with Gemini integration

Backend (FastAPI):
- 13 routers: market_data, financials, valuation, technical, earnings, insider, edgar, news, portfolio, analysis, chat, estimates, fx
- Services: DCF engine, Monte Carlo simulation, sensitivity analysis, risk metrics, SEC parser, technical indicators
- yfinance + yahooquery data sources with fallback pattern
- SQLite caching layer

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 02:10:10 +00:00

70 lines
3.0 KiB
Python

"""
Portfolio sidebar widgets — earnings calendar, dividends, news.
Split from tab9_portfolio.py to stay under 300-line limit.
"""
import streamlit as st
from data.portfolio import (
get_earnings_calendar, get_dividend_schedule, get_portfolio_news,
)
def render_earnings(tickers: tuple):
"""Earnings calendar widget."""
st.markdown("#### Earnings Calendar")
events = get_earnings_calendar(tickers)
if not events:
st.caption("No upcoming earnings found.")
return
for ev in events[:8]:
d = ev["days_until"]
color = "#F87171" if d <= 3 else "#FBBF24" if d <= 7 else "#6B7280"
st.markdown(
f'<div style="display:flex;align-items:center;gap:10px;padding:6px 0;'
f'border-bottom:1px solid rgba(255,255,255,0.06);">'
f'<span style="background:{color};color:#fff;padding:2px 8px;border-radius:4px;'
f'font-size:0.75rem;font-weight:700;min-width:40px;text-align:center;">D-{d}</span>'
f'<div><span style="color:#F3F4F6;font-weight:600;">{ev["name"]}</span>'
f'<br><span style="color:#6B7280;font-size:0.75rem;">{ev["ticker"]} · {ev["date"]}</span></div>'
f'</div>', unsafe_allow_html=True
)
def render_dividends(tickers: tuple):
"""Dividend schedule widget."""
st.markdown("#### Dividend Schedule")
divs = get_dividend_schedule(tickers)
if not divs:
st.caption("No dividend data found.")
return
for d in divs[:8]:
st.markdown(
f'<div style="padding:6px 0;border-bottom:1px solid rgba(255,255,255,0.06);">'
f'<span style="color:#F3F4F6;font-weight:600;">{d["name"]}</span>'
f' <span style="color:#6B7280;font-size:0.8rem;">{d["ticker"]}</span><br>'
f'<span style="color:#34D399;font-size:0.85rem;">Yield: {d["yield_pct"]}</span>'
f' · <span style="color:#9CA3AF;font-size:0.8rem;">Ex-Date: {d["ex_date"]}</span>'
f' · <span style="color:#9CA3AF;font-size:0.8rem;">Annual: {d["amount"]}</span>'
f'</div>', unsafe_allow_html=True
)
def render_news(tickers: tuple):
"""Recent news for portfolio stocks."""
st.markdown("#### Portfolio News")
news = get_portfolio_news(tickers, max_per_ticker=2)
if not news:
st.caption("No recent news.")
return
from datetime import datetime
for n in news[:10]:
ts = n.get("published", 0)
date_str = datetime.fromtimestamp(ts).strftime("%m/%d") if ts else ""
link = n.get("link", "#")
st.markdown(
f'<div style="padding:6px 0;border-bottom:1px solid rgba(255,255,255,0.06);">'
f'<span style="color:#60A5FA;font-weight:600;font-size:0.8rem;">{n["ticker"]}</span>'
f' <span style="color:#6B7280;font-size:0.75rem;">{n.get("publisher","")} · {date_str}</span><br>'
f'<a href="{link}" target="_blank" style="color:#F3F4F6;text-decoration:none;font-size:0.85rem;">'
f'{n["title"]}</a></div>', unsafe_allow_html=True
)