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>
This commit is contained in:
shawnkim1997
2026-03-21 02:10:10 +00:00
co-authored by Claude Opus 4.6
parent 56a9561f71
commit b2acda81ee
111 changed files with 13883 additions and 270 deletions
+88
View File
@@ -0,0 +1,88 @@
"""Numeric safety utilities for the ATLAS Terminal backend.
Provides safe type-coercion helpers used across all services to handle
None, NaN, and non-numeric values gracefully without raising exceptions.
"""
from typing import Optional
import pandas as pd
def _safe_float(x: object) -> Optional[float]:
"""Convert *x* to ``float``, returning ``None`` for unconvertible values.
Handles ``None``, ``NaN`` (both Python ``float('nan')`` and pandas
``pd.NA``), and arbitrary objects whose ``float()`` conversion fails.
Parameters
----------
x:
Any value that might be numeric.
Returns
-------
Optional[float]
The float representation, or ``None`` if conversion is impossible.
"""
if x is None or (isinstance(x, float) and (x != x or pd.isna(x))):
return None
try:
return float(x)
except (TypeError, ValueError):
return None
def _na(x: object) -> object:
"""Return the string ``'N/A'`` for ``None``/``NaN``, otherwise *x* unchanged.
Useful when building display-ready dictionaries or DataFrames where
missing numeric values should appear as a human-readable sentinel.
Parameters
----------
x:
Any value.
Returns
-------
object
``'N/A'`` when *x* is ``None`` or ``NaN``; *x* otherwise.
"""
if x is None or (isinstance(x, float) and (pd.isna(x) or x != x)):
return "N/A"
return x
def _format_shares_display(shares: Optional[float]) -> str:
"""Format a share count for human-friendly display.
Examples
--------
>>> _format_shares_display(15_420_000_000)
'15.42B Shares'
>>> _format_shares_display(1_200_000)
'1.20M Shares'
>>> _format_shares_display(None)
'N/A'
Parameters
----------
shares:
Raw share count (absolute number, not in millions/billions).
Returns
-------
str
A concise string such as ``'15.42B Shares'`` or ``'N/A'``.
"""
if shares is None or shares <= 0:
return "N/A"
s = float(shares)
if s >= 1e9:
return f"{s / 1e9:.2f}B Shares"
if s >= 1e6:
return f"{s / 1e6:.2f}M Shares"
if s >= 1e3:
return f"{s / 1e3:.2f}K Shares"
return f"{s:.0f} Shares"
+122
View File
@@ -0,0 +1,122 @@
"""Ticker formatting, market inference, and company/sector reference data.
Centralises the mapping logic that converts bare ticker symbols into
Yahoo Finance-compatible identifiers with the correct market suffix,
and provides the static lookup tables for companies and sectors.
"""
from typing import List, Tuple
# ---------------------------------------------------------------------------
# Company reference data
# ---------------------------------------------------------------------------
COMPANY_LIST: List[Tuple[str, str]] = [
("NVIDIA Corporation", "NVDA"), ("Apple Inc.", "AAPL"), ("Microsoft Corporation", "MSFT"),
("Amazon.com Inc.", "AMZN"), ("Alphabet Inc.", "GOOGL"), ("Meta Platforms Inc.", "META"),
("AMD", "AMD"), ("Intel Corporation", "INTC"), ("Qualcomm Inc.", "QCOM"), ("Tesla Inc.", "TSLA"),
("Berkshire Hathaway", "BRK.B"), ("JPMorgan Chase", "JPM"), ("Visa Inc.", "V"),
("UnitedHealth", "UNH"), ("Procter & Gamble", "PG"), ("Exxon Mobil", "XOM"),
("Johnson & Johnson", "JNJ"), ("Mastercard", "MA"), ("Chevron", "CVX"),
("Home Depot", "HD"), ("Merck", "MRK"), ("AbbVie", "ABBV"), ("Costco", "COST"),
("PepsiCo", "PEP"), ("Coca-Cola", "KO"), ("Pfizer", "PFE"), ("Walmart", "WMT"),
("Netflix", "NFLX"), ("Adobe", "ADBE"), ("Salesforce", "CRM"), ("Comcast", "CMCSA"),
("Cisco", "CSCO"), ("Oracle", "ORCL"), ("American Express", "AXP"),
("Bank of America", "BAC"), ("Wells Fargo", "WFC"), ("Verizon", "VZ"),
("AT&T", "T"), ("Walt Disney", "DIS"), ("Nike", "NKE"), ("McDonald's", "MCD"),
("Starbucks", "SBUX"), ("Goldman Sachs", "GS"), ("Morgan Stanley", "MS"),
("Target", "TGT"), ("Boeing", "BA"), ("IBM", "IBM"),
]
COMPANY_OPTIONS: List[str] = [f"{t} - {n}" for n, t in COMPANY_LIST]
"""Pre-formatted ``'TICKER - Company Name'`` strings for dropdowns."""
COMPANY_TICKER_MAP: dict[str, str] = {t: n for n, t in COMPANY_LIST}
"""Mapping from ticker symbol to full company name."""
MARKET_OPTIONS: List[str] = [
"US (S&P/Dow/Nasdaq)",
"South Korea (KOSPI/KOSDAQ)",
"Japan (Nikkei)",
"UK (LSE)",
]
# ---------------------------------------------------------------------------
# Sector / industry peer groups (top-down analysis)
# ---------------------------------------------------------------------------
SECTORS: dict[str, List[str]] = {
"Semiconductors & Hardware": ["NVDA", "AMD", "INTC", "TSM", "AVGO"],
"Software & Cloud": ["MSFT", "ADBE", "CRM", "PANW", "CRWD"],
"Consumer Retail": ["AMZN", "SBUX", "MCD", "WMT", "HD"],
"Financial Services": ["JPM", "BAC", "GS", "MS", "V"],
"Healthcare": ["LLY", "UNH", "JNJ", "ABBV", "MRK"],
}
# ---------------------------------------------------------------------------
# Ticker helpers
# ---------------------------------------------------------------------------
def get_global_ticker(ticker: str, market: str) -> str:
"""Append the correct Yahoo Finance suffix based on the selected market.
US tickers are returned as-is. If the ticker already carries a known
suffix (``.KS``, ``.KQ``, ``.T``, ``.L``) it is returned unchanged
regardless of the *market* argument.
Parameters
----------
ticker:
Raw ticker string entered by the user.
market:
One of the values in :data:`MARKET_OPTIONS`.
Returns
-------
str
The ticker with an appropriate suffix (or unchanged for US).
"""
if not (ticker or "").strip():
return (ticker or "").strip()
t = (ticker or "").strip()
if t.upper().endswith((".KS", ".KQ", ".T", ".L")):
return t
m = (market or "").strip()
if "US" in m or not m:
return t
if "Korea" in m or "KOSPI" in m or "KOSDAQ" in m:
return t + ".KS"
if "Japan" in m or "Nikkei" in m:
return t + ".T"
if "UK" in m or "LSE" in m:
return t + ".L"
return t
def infer_market_from_ticker(ticker: str) -> str:
"""Guess the market label from a ticker's suffix.
Useful when the caller has a fully-qualified ticker (e.g. ``005930.KS``)
but no explicit market selection.
Parameters
----------
ticker:
A ticker string that may include a market suffix.
Returns
-------
str
The best-matching entry from :data:`MARKET_OPTIONS`.
"""
if not (ticker or "").strip():
return MARKET_OPTIONS[0]
t = (ticker or "").strip().upper()
if t.endswith(".KS") or t.endswith(".KQ"):
return "South Korea (KOSPI/KOSDAQ)"
if t.endswith(".T"):
return "Japan (Nikkei)"
if t.endswith(".L"):
return "UK (LSE)"
return "US (S&P/Dow/Nasdaq)"