Files
All-in-one-Financial-Analysis/atlas-terminal/server/ai/context_builder.py
T
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

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()