mirror of
https://github.com/shawnkim1997/All-in-one-Financial-Analysis.git
synced 2026-08-17 04:18:08 +00:00
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>
223 lines
7.6 KiB
Python
223 lines
7.6 KiB
Python
"""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()
|