mirror of
https://github.com/shawnkim1997/All-in-one-Financial-Analysis.git
synced 2026-08-24 07:58:05 +00:00
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:
co-authored by
Claude Opus 4.6
parent
56a9561f71
commit
b2acda81ee
@@ -0,0 +1,222 @@
|
||||
"""Builds AI context from active widget data for the ATLAS Terminal chat."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Widget-specific context templates
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_DCF_TEMPLATE = """
|
||||
## DCF Valuation Context
|
||||
- Implied share price: ${implied_price:.2f}
|
||||
- Current market price: ${current_price:.2f}
|
||||
- Upside/Downside: {upside:+.1f}%
|
||||
- WACC: {wacc:.1f}%
|
||||
- Terminal growth rate: {terminal_growth:.1f}%
|
||||
- FCF projections (5Y): {fcf_projections}
|
||||
"""
|
||||
|
||||
_FINANCIALS_TEMPLATE = """
|
||||
## Financial Metrics Context
|
||||
- Revenue (TTM): ${revenue}
|
||||
- Net income (TTM): ${net_income}
|
||||
- Gross margin: {gross_margin:.1f}%
|
||||
- Operating margin: {operating_margin:.1f}%
|
||||
- ROE: {roe:.1f}%
|
||||
- Debt/Equity: {debt_equity:.2f}
|
||||
- Current ratio: {current_ratio:.2f}
|
||||
"""
|
||||
|
||||
_TECHNICAL_TEMPLATE = """
|
||||
## Technical Analysis Context
|
||||
- RSI (14): {rsi:.1f}
|
||||
- MACD: {macd:.4f} | Signal: {macd_signal:.4f}
|
||||
- SMA 50: ${sma_50:.2f} | SMA 200: ${sma_200:.2f}
|
||||
- 52-week high: ${high_52w:.2f} | Low: ${low_52w:.2f}
|
||||
- Volume (avg 20d): {avg_volume}
|
||||
"""
|
||||
|
||||
_PORTFOLIO_TEMPLATE = """
|
||||
## Portfolio Context
|
||||
- Total value: ${total_value:,.0f}
|
||||
- Number of positions: {position_count}
|
||||
- Top holdings: {top_holdings}
|
||||
- Sector allocation: {sector_allocation}
|
||||
"""
|
||||
|
||||
_FILING_TEMPLATE = """
|
||||
## SEC Filing Context
|
||||
- Latest filing type: {filing_type}
|
||||
- Filed on: {filing_date}
|
||||
- Key sections available: {sections}
|
||||
"""
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Suggested questions per widget
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_WIDGET_QUESTIONS: dict[str, list[str]] = {
|
||||
"dcf": [
|
||||
"Are the market's growth assumptions reasonable for this company?",
|
||||
"What would the fair value be with a higher discount rate?",
|
||||
"How sensitive is the valuation to terminal growth assumptions?",
|
||||
],
|
||||
"financials": [
|
||||
"How do the margins compare to industry peers?",
|
||||
"Is the revenue growth trend sustainable?",
|
||||
"What are the key drivers behind the profitability changes?",
|
||||
],
|
||||
"technical": [
|
||||
"What does the current technical setup suggest for the near term?",
|
||||
"Is the stock overbought or oversold based on RSI?",
|
||||
"Are there any notable divergences between price and momentum?",
|
||||
],
|
||||
"portfolio": [
|
||||
"Is my sector diversification sufficient?",
|
||||
"Which positions carry the most concentration risk?",
|
||||
"How does my portfolio beta compare to the market?",
|
||||
],
|
||||
"filing": [
|
||||
"What are the key risks disclosed in the latest filing?",
|
||||
"Are there any notable changes in accounting policies?",
|
||||
"What does management say about the competitive landscape?",
|
||||
],
|
||||
"news": [
|
||||
"What is the overall sentiment of recent news?",
|
||||
"Are there any material events that could affect the stock?",
|
||||
"How might recent headlines impact the company's outlook?",
|
||||
],
|
||||
}
|
||||
|
||||
_DEFAULT_QUESTIONS: list[str] = [
|
||||
"Give me a quick overview of this company's financial health.",
|
||||
"What are the biggest risks facing this stock right now?",
|
||||
"Should I consider adding this to my portfolio? Why or why not?",
|
||||
]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ContextBuilder
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class ContextBuilder:
|
||||
"""Builds AI context from active widget data.
|
||||
|
||||
The context is injected into the system prompt so the LLM can reference
|
||||
concrete numbers when answering the user's questions about a ticker.
|
||||
"""
|
||||
|
||||
def build_system_prompt(
|
||||
self,
|
||||
ticker: str,
|
||||
active_widgets: list[str],
|
||||
widget_data: dict[str, Any],
|
||||
) -> str:
|
||||
"""Build a context-aware system prompt.
|
||||
|
||||
Args:
|
||||
ticker: The active ticker symbol (e.g. ``"AAPL"``).
|
||||
active_widgets: List of widget identifiers currently visible
|
||||
(e.g. ``["dcf", "financials", "technical"]``).
|
||||
widget_data: A dict keyed by widget name containing the data
|
||||
displayed in each widget.
|
||||
|
||||
Returns:
|
||||
A system prompt string enriched with financial context.
|
||||
"""
|
||||
sections: list[str] = [
|
||||
"You are ATLAS, an expert financial analyst assistant "
|
||||
"integrated into the ATLAS Terminal.\n"
|
||||
"You have direct access to the data the user is currently viewing.\n"
|
||||
"Answer concisely with concrete numbers when available. "
|
||||
"Use markdown formatting for readability.",
|
||||
]
|
||||
|
||||
# Always include base info if available
|
||||
base = widget_data.get("base", {})
|
||||
if ticker:
|
||||
sections.append(
|
||||
f"\n## Active Ticker: {ticker.upper()}\n"
|
||||
f"- Sector: {base.get('sector', 'N/A')}\n"
|
||||
f"- Current price: ${base.get('current_price', 'N/A')}\n"
|
||||
f"- Market cap: {base.get('market_cap', 'N/A')}\n"
|
||||
)
|
||||
|
||||
# Append widget-specific context
|
||||
for widget in active_widgets:
|
||||
section = self._build_widget_section(widget, widget_data)
|
||||
if section:
|
||||
sections.append(section)
|
||||
|
||||
return "\n".join(sections)
|
||||
|
||||
def build_suggested_questions(
|
||||
self,
|
||||
active_widgets: list[str],
|
||||
) -> list[str]:
|
||||
"""Generate suggested questions based on active widgets.
|
||||
|
||||
Args:
|
||||
active_widgets: List of widget identifiers currently visible.
|
||||
|
||||
Returns:
|
||||
A list of 3-5 suggested question strings.
|
||||
"""
|
||||
questions: list[str] = []
|
||||
|
||||
for widget in active_widgets:
|
||||
widget_key = widget.lower().strip()
|
||||
if widget_key in _WIDGET_QUESTIONS:
|
||||
# Pick the first question from each active widget
|
||||
questions.append(_WIDGET_QUESTIONS[widget_key][0])
|
||||
|
||||
# Pad with defaults if we have fewer than 3
|
||||
for q in _DEFAULT_QUESTIONS:
|
||||
if len(questions) >= 5:
|
||||
break
|
||||
if q not in questions:
|
||||
questions.append(q)
|
||||
|
||||
return questions[:5]
|
||||
|
||||
# -- private helpers ----------------------------------------------------
|
||||
|
||||
def _build_widget_section(
|
||||
self, widget: str, widget_data: dict[str, Any]
|
||||
) -> str:
|
||||
"""Render context section for a specific widget.
|
||||
|
||||
Returns an empty string when no data is available for the widget.
|
||||
"""
|
||||
widget_key = widget.lower().strip()
|
||||
data = widget_data.get(widget_key, {})
|
||||
|
||||
if not data:
|
||||
return ""
|
||||
|
||||
try:
|
||||
if widget_key == "dcf":
|
||||
return _DCF_TEMPLATE.format(**data)
|
||||
if widget_key == "financials":
|
||||
return _FINANCIALS_TEMPLATE.format(**data)
|
||||
if widget_key == "technical":
|
||||
return _TECHNICAL_TEMPLATE.format(**data)
|
||||
if widget_key == "portfolio":
|
||||
return _PORTFOLIO_TEMPLATE.format(**data)
|
||||
if widget_key == "filing":
|
||||
return _FILING_TEMPLATE.format(**data)
|
||||
except (KeyError, ValueError, TypeError) as exc:
|
||||
# Gracefully degrade -- partial data is fine
|
||||
return f"\n## {widget.title()} Context\nPartial data: {data}\n"
|
||||
|
||||
# Unknown widget -- dump raw data as a summary
|
||||
return f"\n## {widget.title()} Context\n{data}\n"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Singleton
|
||||
# ---------------------------------------------------------------------------
|
||||
context_builder = ContextBuilder()
|
||||
Reference in New Issue
Block a user