Files
All-in-one-Financial-Analysis/ai/gemini_insights.py
T
shawnkim1997andClaude Sonnet 4.6 d337c63976 refactor: modular architecture v3.0 + SEC filing viewer fix + README
Architecture (3,909-line monolith → 28 focused modules, all < 300 lines):
- config/: constants.py (company lists, row maps, Damodaran baselines), theme.py (CSS/HTML)
- utils/: prefs, formatting, ticker, dcf, charts, ui_helpers
- data/: sec_parser, sec_fetcher, sec_downloader, financials, fundamentals,
         valuation, ratios, scores, scores_ai, market
- ai/: gemini_core, gemini_sec, gemini_insights
- views/: sidebar, tab1_quant, tab1_ai, tab1_filings, tab2_dcf,
          tab3_comps, tab4_news, tab5_markets, tab6_crypto, tab7_technical
- app.py: thin orchestrator (~118 lines)
- Strict unidirectional dependency graph (no circular imports)
- All @st.cache_data TTLs and st.session_state keys preserved identically

SEC filing viewer fix:
- Rebuilt EDGAR fetch chain: company_tickers.json → CIK → submissions API
  → filings.recent.primaryDocument[] (replaces deprecated directory.item)
- Filing type selectbox (10-K, 10-Q, 8-K, 20-F, 6-K) connected to backend
- Native HTML rendered via streamlit.components.v1.html() with CSS reset
- Errors surfaced explicitly with st.error()
- DART direct links restored for Korean-listed companies

.gitignore: data/ → data/*.json + data/*.html (preserve Python modules)
README: full rewrite for master's portfolio — 7-tab layout, architecture
diagram, modular structure tree, technical challenges, design rationale

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 22:11:07 +00:00

192 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from typing import Optional
import streamlit as st
from data.sec_parser import smart_chunk, clean_text_for_llm
from ai.gemini_core import (
get_gemini_model, _generate_with_retry, _is_rate_limit_error,
_split_into_chunks, _gemini_summarize_segment, _gemini_synthesize_report, _gemini_forensic_audit,
)
def get_mda_chunked_insights(
api_key: str, sections: dict, ticker: str, sector: str, industry: str, progress_callback=None
) -> str:
"""Full-text analysis: chunk 1A+7, summarize each segment, synthesize report; then append forensic (Item 3, 9A). progress_callback(step: str) optional."""
def _progress(msg):
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=22000)
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:
"""Send Item 1A + Item 7 to Gemini. Analyse: 1) Management's Tone (Sentiment), 2) Key Strategic Shifts, 3) Major Hidden 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 = "\n\n---\n\n".join(combined)
combined_text = smart_chunk(combined_text, max_chars=22000)
user_prompt = f"""You are a senior equity analyst. Use British English.
The text below is from the 10-K for {ticker}: **Item 1A (Risk Factors)** and **Item 7 (Management's Discussion and Analysis)**. HTML has been stripped; analyse only the substance.
Provide a concise report with three sections:
1. **Management's Tone (Sentiment)**: Is the overall tone positive, cautious, or negative? Quote 12 short phrases that support your view.
2. **Key Strategic Shifts**: What strategic priorities or shifts does management emphasise (e.g. capital allocation, growth drivers, new segments)? Be specific.
3. **Major Hidden Risks**: From both Risk Factors and MD&A, what are the 34 most material risks that an investor might overlook? Cite the document.
Use clear headings. Do not invent figures. Keep the response focused and under 800 words."""
full_content = f"""--- 10-K Excerpt (Item 1A + Item 7) ---\n\n{combined_text}\n\n---\n\n{user_prompt}"""
try:
response = _generate_with_retry(
model, full_content, {"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 analysis: if item7_3y_ago provided, compare MD&As over 3 years; else single-year. Sector-aware: extract industry-specific Non-GAAP 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 "")
+ ", meticulously scan the MD&A to find and extract **industry-specific Non-GAAP KPIs** "
"(e.g. Same-Store Sales Growth for Retail, ARR/NDR for Software, DAU/MAU for Tech). Present these hidden KPIs in a **clean markdown table** with columns such as KPI name, value, and period if stated."
)
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 = "\n\n---\n\n".join(combined)
combined_text = smart_chunk(combined_text, max_chars=22000)
user_prompt = f"""You are a senior equity analyst. Use British English.
The text below is from the **latest 10-K only** for {ticker}: **Item 1A (Risk Factors)** and **Item 7 (MD&A)**. Provide a focused deep-dive report:
1. **Management's Tone (Sentiment)**: Overall tone and 12 supporting phrases.
2. **Current Strategy & Priorities**: Key strategic focus, capital allocation, growth drivers from this filing only.
3. **Major Hidden Risks**: From Item 1A and MD&A, the 34 most material risks investors might overlook.
4. **Forensic / Quality of Earnings**: Any red flags in MD&A (accounting caveats, one-offs, cash flow vs earnings, segment disclosure). If none material, say so briefly.{kpi_instruction}
**Token-saving (Item 3 / 9A):** If no material weaknesses, major lawsuits, or off-balance-sheet red flags, output exactly: "\u2705 No material red flags or special issues detected in Item 3 and 9A."
Use clear headings. Under 800 words."""
full_content = f"""--- 10-K Excerpt (Latest Year) ---\n\n{combined_text}\n\n---\n\n{user_prompt}"""
else:
latest_clean = smart_chunk(clean_text_for_llm(item7_latest), max_chars=12000)
past_clean = smart_chunk(clean_text_for_llm(item7_3y_ago), max_chars=12000)
user_prompt = f"""You are a senior equity analyst. Use British English.
Below are **Item 7 (Management's Discussion and Analysis)** from the 10-K for {ticker}: **LATEST YEAR** and **THREE YEARS AGO**. Perform a **Comparative Analysis**.
1. **Core strategy**: What has changed in the company's stated strategy, priorities, or capital allocation between then and now?
2. **Emerging risks**: What new risks appear in the latest MD&A that were absent or less prominent 3 years ago?
3. **Management's tone**: How has the overall tone (confidence, caution, optimism) shifted? Quote 12 phrases from each period if relevant.
4. **Industry-specific KPIs**:{kpi_instruction}
5. **Item 3 (Legal) & Item 9A (Internal Controls):** You must save output tokens. If there are no material weaknesses, no massive lawsuits, and no major off-balance sheet red flags, DO NOT generate a long explanation. Simply output exactly: "\u2705 No material red flags or special issues detected in Item 3 and 9A." and move on.
Use clear headings. Do not invent figures. Keep the response focused and under 900 words."""
full_content = f"""--- MD&A LATEST YEAR ---\n\n{latest_clean}\n\n--- MD&A THREE YEARS AGO ---\n\n{past_clean}\n\n---\n\n{user_prompt}"""
try:
response = _generate_with_retry(
model, full_content, {"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 _run_mda_analysis_background(ticker: str, api_key: str, sec_email: str) -> None:
"""Run download + Gemini in background (latest 10-K only for speed). Store result or error in st.session_state."""
try:
from data.sec_downloader import download_and_extract_item7_and_1a
from data.fundamentals import get_sector_industry
_, item1a, item7_latest = download_and_extract_item7_and_1a(ticker, sec_email)
si = get_sector_industry(ticker)
analysis = get_mda_comparative_insights(
api_key, item1a or "", item7_latest or "", None, ticker,
sector=si.get("sector"), industry=si.get("industry"),
)
st.session_state["mda_analysis_result"] = analysis
st.session_state["mda_analysis_excerpt"] = ((item1a or "") + "\n\n---\n\n" + (item7_latest or ""))[:12000]
st.session_state["mda_analysis_error"] = None
except Exception as e:
st.session_state["mda_analysis_error"] = str(e)
st.session_state["mda_analysis_result"] = None
st.session_state["mda_analysis_excerpt"] = None
finally:
st.session_state["mda_analysis_running"] = False
st.session_state["mda_analysis_done"] = True
st.session_state["mda_analysis_ticker"] = ticker
def get_industry_outlook(api_key: str, industry_name: str, tickers: list) -> str:
"""Gemini: Wall Street macro analyst-style Industry Outlook for the selected sector (12\u201318 months)."""
model = get_gemini_model(api_key)
ticker_list_str = ", ".join(str(t).upper() for t in tickers if t)
user_prompt = f"""Act as an elite Wall Street macro analyst. Provide a concise **Industry Outlook** report for the **{industry_name}** sector, which includes leading companies like {ticker_list_str}.
Focus on:
1. **Macro trends** affecting this industry over the next 12\u201318 months.
2. **Major growth drivers** (e.g., AI, interest rates, consumer spending, regulation).
3. **Key headwinds or regulatory risks** that could impact valuations or growth.
Use clear headings. Be specific but concise. Keep the response under 600 words."""
full_content = user_prompt
try:
response = _generate_with_retry(
model, full_content, {"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()