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>
205 lines
7.6 KiB
Python
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()
|