Files
All-in-one-Financial-Analysis/atlas-terminal/server/services/gemini_analysis.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

205 lines
7.6 KiB
Python

"""High-level Gemini analysis orchestrators.
Contains the composite analysis functions that combine multiple Gemini
calls (chunked insights, comparative MD&A, industry outlook). These build
on the primitives in :mod:`server.services.gemini_service`.
"""
from typing import Any, Callable, Dict, Optional
from server.services.gemini_service import (
_gemini_forensic_audit,
_gemini_summarize_segment,
_gemini_synthesize_report,
_generate_with_retry,
_is_rate_limit_error,
get_gemini_model,
)
from server.services.text_chunker import clean_text_for_llm, smart_chunk, _split_into_chunks
def get_mda_chunked_insights(
api_key: str,
sections: Dict[str, str],
ticker: str,
sector: str,
industry: str,
progress_callback: Optional[Callable[[str], None]] = None,
) -> str:
"""Full-text analysis: chunk 1A+7, summarise each, synthesise, then append forensic.
Parameters
----------
api_key:
Google Gemini API key.
sections:
Dict with keys ``item1a``, ``item7``, ``item3``, ``item9a``.
ticker:
Stock ticker symbol.
sector / industry:
Used for sector-aware KPI extraction.
progress_callback:
Optional ``fn(msg: str)`` called with status updates.
Returns
-------
str
Markdown-formatted Executive Insight Report.
"""
def _progress(msg: str) -> None:
if progress_callback:
progress_callback(msg)
combined = (sections.get("item1a") or "") + "\n\n---\n\n" + (sections.get("item7") or "")
combined = combined.strip()
if not combined:
return "No 10-K text available to analyse."
chunks = _split_into_chunks(combined, max_chars=22_000)
if not chunks:
return "No content extracted."
summaries = []
n = len(chunks)
for i, ch in enumerate(chunks):
_progress(f"Analyzing Segment {i + 1}/{n}...")
summary = _gemini_summarize_segment(api_key, ch, ticker, f"Segment {i + 1}/{n}")
if summary:
summaries.append(summary)
if not summaries:
return "Segment analysis produced no summaries."
_progress("Synthesizing final report...")
report = _gemini_synthesize_report(api_key, summaries, ticker, sector or "N/A", industry or "N/A")
_progress("Running forensic audit (Item 3 & 9A)...")
forensic = _gemini_forensic_audit(api_key, sections.get("item3") or "", sections.get("item9a") or "", ticker)
return (report or "") + "\n\n---\n\n**Forensic (Item 3 & 9A)**\n\n" + (forensic or "")
def get_mda_insights(
api_key: str,
item1a_text: str,
item7_text: str,
ticker: str,
) -> str:
"""Single-shot analysis of Item 1A + Item 7 (tone, strategy, risks)."""
model = get_gemini_model(api_key)
combined = []
if item1a_text:
combined.append(clean_text_for_llm(item1a_text))
if item7_text:
combined.append(clean_text_for_llm(item7_text))
combined_text = smart_chunk("\n\n---\n\n".join(combined), max_chars=22_000)
prompt = (
f"You are a senior equity analyst. Use British English.\n\n"
f"The text below is from the 10-K for {ticker}: **Item 1A** and **Item 7**.\n\n"
"Provide a concise report:\n"
"1. **Management's Tone (Sentiment)**\n"
"2. **Key Strategic Shifts**\n"
"3. **Major Hidden Risks**\n\n"
"Use clear headings. Under 800 words."
)
full = f"--- 10-K Excerpt ---\n\n{combined_text}\n\n---\n\n{prompt}"
try:
response = _generate_with_retry(model, full, {"temperature": 0.3, "max_output_tokens": 4096})
except Exception as api_err:
if _is_rate_limit_error(api_err):
raise RuntimeError("Rate limit exceeded. Please try again in a few minutes.") from api_err
raise
if not response or not response.text:
return "No analysis generated."
return response.text.strip()
def get_mda_comparative_insights(
api_key: str,
item1a_text: str,
item7_latest: str,
item7_3y_ago: Optional[str],
ticker: str,
sector: Optional[str] = None,
industry: Optional[str] = None,
) -> str:
"""Comparative or single-year MD&A deep-dive with sector-aware KPIs."""
model = get_gemini_model(api_key)
sector_label = (sector or "N/A").strip()
industry_label = (industry or "N/A").strip()
kpi_instruction = (
f" Given that this company is in the **{sector_label}** sector"
+ (f" (industry: {industry_label})" if industry_label != "N/A" else "")
+ ", extract **industry-specific Non-GAAP KPIs** in a markdown table."
)
if not item7_3y_ago or not item7_3y_ago.strip():
combined = []
if item1a_text:
combined.append(clean_text_for_llm(item1a_text))
if item7_latest:
combined.append(clean_text_for_llm(item7_latest))
combined_text = smart_chunk("\n\n---\n\n".join(combined), max_chars=22_000)
prompt = (
f"You are a senior equity analyst. Use British English.\n"
f"Latest 10-K only for {ticker} (Item 1A + Item 7). Provide:\n"
"1. **Management's Tone**\n2. **Current Strategy & Priorities**\n"
"3. **Major Hidden Risks**\n4. **Forensic / Quality of Earnings**\n"
f"{kpi_instruction}\nUnder 800 words."
)
full = f"--- 10-K Excerpt (Latest Year) ---\n\n{combined_text}\n\n---\n\n{prompt}"
else:
latest_clean = smart_chunk(clean_text_for_llm(item7_latest), max_chars=12_000)
past_clean = smart_chunk(clean_text_for_llm(item7_3y_ago), max_chars=12_000)
prompt = (
f"You are a senior equity analyst. Use British English.\n"
f"Below are Item 7 from the 10-K for {ticker}: LATEST and THREE YEARS AGO.\n"
"1. **Core strategy** changes\n2. **Emerging risks**\n"
"3. **Management's tone** shift\n4. **Industry-specific KPIs**\n"
f"{kpi_instruction}\nUnder 900 words."
)
full = (
f"--- MD&A LATEST YEAR ---\n\n{latest_clean}\n\n"
f"--- MD&A THREE YEARS AGO ---\n\n{past_clean}\n\n---\n\n{prompt}"
)
try:
response = _generate_with_retry(model, full, {"temperature": 0.3, "max_output_tokens": 4096})
except Exception as api_err:
if _is_rate_limit_error(api_err):
raise RuntimeError("Rate limit exceeded. Please try again in a few minutes.") from api_err
raise
if not response or not response.text:
return "No analysis generated."
return response.text.strip()
def get_industry_outlook(
api_key: str,
industry_name: str,
tickers: list,
) -> str:
"""Generate a Wall Street macro-analyst-style Industry Outlook (12-18 months)."""
model = get_gemini_model(api_key)
ticker_list_str = ", ".join(str(t).upper() for t in tickers if t)
prompt = (
f"Act as an elite Wall Street macro analyst. Provide a concise "
f"**Industry Outlook** for the **{industry_name}** sector, "
f"which includes companies like {ticker_list_str}.\n\n"
"Focus on:\n"
"1. **Macro trends** (next 12-18 months)\n"
"2. **Major growth drivers**\n"
"3. **Key headwinds or regulatory risks**\n\n"
"Use clear headings. Under 600 words."
)
try:
response = _generate_with_retry(model, prompt, {"temperature": 0.4, "max_output_tokens": 2048})
except Exception as api_err:
if _is_rate_limit_error(api_err):
raise RuntimeError("Rate limit exceeded. Please try again in a few minutes.") from api_err
raise
if not response or not response.text:
return "No industry outlook generated."
return response.text.strip()