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
@@ -0,0 +1,116 @@
"""Sensitivity analysis for DCF valuation.
Provides:
- WACC vs Terminal Growth sensitivity matrix
- Tornado chart data (variable impact ranking)
"""
from typing import Dict, List, Any
from server.services.dcf_engine import excel_style_dcf
def build_sensitivity_matrix(
fcf: float,
total_debt: float,
cash: float,
shares: float,
base_wacc: float,
base_tg: float,
fcf_growth: float,
wacc_steps: int = 6,
tg_steps: int = 5,
wacc_range: float = 0.02,
tg_range: float = 0.01,
) -> Dict[str, Any]:
"""Build a 2-D sensitivity matrix: WACC (rows) x Terminal Growth (cols).
Returns
-------
dict
wacc_values : list[float] row headers (percentages, e.g. 8.0)
tg_values : list[float] column headers (percentages, e.g. 2.5)
matrix : list[list[float | None]] per-share intrinsic values
"""
# Generate evenly-spaced WACC and TG values centred on base
wacc_values = [
round(base_wacc - wacc_range + (2 * wacc_range / max(wacc_steps - 1, 1)) * i, 4)
for i in range(wacc_steps)
]
tg_values = [
round(base_tg - tg_range + (2 * tg_range / max(tg_steps - 1, 1)) * i, 4)
for i in range(tg_steps)
]
matrix: List[List[Any]] = []
for w in wacc_values:
row: List[Any] = []
for tg in tg_values:
if w <= tg or w <= 0 or shares <= 0:
row.append(None)
else:
result = excel_style_dcf(fcf, w, tg, fcf_growth, total_debt, cash, shares)
vps = result.get("value_per_share")
row.append(round(vps, 2) if vps is not None else None)
matrix.append(row)
return {
"wacc_values": [round(w * 100, 2) for w in wacc_values],
"tg_values": [round(tg * 100, 2) for tg in tg_values],
"matrix": matrix,
}
def build_tornado_data(
fcf: float,
wacc: float,
tg: float,
growth: float,
debt: float,
cash: float,
shares: float,
) -> List[Dict[str, Any]]:
"""Compute tornado-chart data by varying each input ±10 %.
Returns a list sorted descending by impact range (high low).
Each entry: {"variable", "low", "high", "base"}.
"""
if shares <= 0:
return []
def _val(f, w, t, g, d, c) -> float | None:
if w <= t or w <= 0:
return None
r = excel_style_dcf(f, w, t, g, d, c, shares)
return r.get("value_per_share")
base_val = _val(fcf, wacc, tg, growth, debt, cash)
if base_val is None:
return []
variables = [
("WACC", lambda sign: _val(fcf, wacc * (1 + sign * 0.10), tg, growth, debt, cash)),
("FCF Growth", lambda sign: _val(fcf, wacc, tg, growth * (1 + sign * 0.10), debt, cash)),
("Terminal Growth", lambda sign: _val(fcf, wacc, tg * (1 + sign * 0.10), growth, debt, cash)),
("Base FCF", lambda sign: _val(fcf * (1 + sign * 0.10), wacc, tg, growth, debt, cash)),
("Total Debt", lambda sign: _val(fcf, wacc, tg, growth, debt * (1 + sign * 0.10), cash)),
("Cash", lambda sign: _val(fcf, wacc, tg, growth, debt, cash * (1 + sign * 0.10))),
]
results: List[Dict[str, Any]] = []
for name, func in variables:
val_up = func(0.10)
val_dn = func(-0.10)
if val_up is None or val_dn is None:
continue
low = round(min(val_up, val_dn), 2)
high = round(max(val_up, val_dn), 2)
results.append({
"variable": name,
"low": low,
"high": high,
"base": round(base_val, 2),
})
results.sort(key=lambda d: d["high"] - d["low"], reverse=True)
return results